summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ackrc2
-rw-r--r--.gitattributes1
-rw-r--r--.gitignore17
-rw-r--r--.gitmodules0
-rw-r--r--.mailmap2
-rw-r--r--.travis.yml35
-rw-r--r--Announcer.hs198
-rw-r--r--ChangeLog115
-rw-r--r--InterruptibleDelay.hs41
-rw-r--r--LICENSE30
-rw-r--r--Makefile6
-rw-r--r--OnionRouter.hs451
-rw-r--r--Roster.hs100
-rw-r--r--TODO.org17
-rw-r--r--bittorrent/README.md78
-rw-r--r--bittorrent/Readme.md8
-rw-r--r--bittorrent/bench/Main.hs75
-rw-r--r--bittorrent/bench/Throughtput.hs46
-rw-r--r--bittorrent/bench/TorrentFile.hs27
-rw-r--r--bittorrent/bittorrent.cabal412
-rw-r--r--bittorrent/dev/README.md4
-rwxr-xr-xbittorrent/dev/add-sources.sh5
-rwxr-xr-xbittorrent/dev/bench4
-rwxr-xr-xbittorrent/dev/test2
-rwxr-xr-xbittorrent/dev/update-dependencies.sh11
-rw-r--r--bittorrent/examples/Client.hs74
-rw-r--r--bittorrent/examples/FS.hs74
-rw-r--r--bittorrent/examples/MkTorrent.hs500
-rw-r--r--bittorrent/res/dapper-dvd-amd64.iso.torrentbin0 -> 64198 bytes
-rw-r--r--bittorrent/res/pkg.torrentbin0 -> 32113 bytes
-rw-r--r--bittorrent/res/testfilebin0 -> 8192 bytes
-rw-r--r--bittorrent/res/testfile.torrent1
-rw-r--r--bittorrent/src/Network/BitTorrent.hs61
-rw-r--r--bittorrent/src/Network/BitTorrent/Client.hs195
-rw-r--r--bittorrent/src/Network/BitTorrent/Client/Handle.hs188
-rw-r--r--bittorrent/src/Network/BitTorrent/Client/Types.hs163
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange.hs35
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs399
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Block.hs369
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Connection.hs1012
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Download.hs296
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Manager.hs62
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Message.hs1232
-rw-r--r--bittorrent/src/Network/BitTorrent/Exchange/Session.hs586
-rw-r--r--bittorrent/src/Network/BitTorrent/Internal/Cache.hs169
-rw-r--r--bittorrent/src/Network/BitTorrent/Internal/Progress.hs154
-rw-r--r--bittorrent/src/Network/BitTorrent/Internal/Types.hs10
-rw-r--r--bittorrent/src/Network/BitTorrent/Readme.md10
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker.hs50
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker/List.hs193
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker/Message.hs920
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker/RPC.hs175
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker/RPC/HTTP.hs191
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker/RPC/UDP.hs454
-rw-r--r--bittorrent/src/Network/BitTorrent/Tracker/Session.hs306
-rw-r--r--bittorrent/src/System/Torrent/FileMap.hs151
-rw-r--r--bittorrent/src/System/Torrent/Storage.hs221
-rw-r--r--bittorrent/src/System/Torrent/Tree.hs83
-rw-r--r--bittorrent/tests/Config.hs183
-rw-r--r--bittorrent/tests/Data/TorrentSpec.hs139
-rw-r--r--bittorrent/tests/Main.hs97
-rw-r--r--bittorrent/tests/Network/BitTorrent/Client/HandleSpec.hs19
-rw-r--r--bittorrent/tests/Network/BitTorrent/CoreSpec.hs305
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHT/MessageSpec.hs221
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHT/QuerySpec.hs105
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHT/RoutingSpec.hs77
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHT/SessionSpec.hs110
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHT/TestData.hs45
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHT/TokenSpec.hs42
-rw-r--r--bittorrent/tests/Network/BitTorrent/DHTSpec.hs60
-rw-r--r--bittorrent/tests/Network/BitTorrent/Exchange/BitfieldSpec.hs14
-rw-r--r--bittorrent/tests/Network/BitTorrent/Exchange/BlockSpec.hs35
-rw-r--r--bittorrent/tests/Network/BitTorrent/Exchange/ConnectionSpec.hs58
-rw-r--r--bittorrent/tests/Network/BitTorrent/Exchange/DownloadSpec.hs59
-rw-r--r--bittorrent/tests/Network/BitTorrent/Exchange/MessageSpec.hs102
-rw-r--r--bittorrent/tests/Network/BitTorrent/Exchange/SessionSpec.hs64
-rw-r--r--bittorrent/tests/Network/BitTorrent/Internal/CacheSpec.hs7
-rw-r--r--bittorrent/tests/Network/BitTorrent/Internal/ProgressSpec.hs13
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/ListSpec.hs40
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/MessageSpec.hs173
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/RPC/HTTPSpec.hs95
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/RPC/UDPSpec.hs144
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/RPCSpec.hs79
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/SessionSpec.hs61
-rw-r--r--bittorrent/tests/Network/BitTorrent/Tracker/TestData.hs93
-rw-r--r--bittorrent/tests/Network/KRPC/MessageSpec.hs72
-rw-r--r--bittorrent/tests/Network/KRPC/MethodSpec.hs52
-rw-r--r--bittorrent/tests/Network/KRPCSpec.hs59
-rw-r--r--bittorrent/tests/Readme.md4
-rw-r--r--bittorrent/tests/Spec.hs1
-rw-r--r--bittorrent/tests/System/Torrent/FileMapSpec.hs116
-rw-r--r--bittorrent/tests/System/Torrent/StorageSpec.hs91
-rwxr-xr-xc8
-rw-r--r--cbits/cryptonite_bitfn.h253
-rw-r--r--cbits/cryptonite_salsa.c297
-rw-r--r--cbits/cryptonite_salsa.h57
-rw-r--r--cbits/cryptonite_xsalsa.c80
-rw-r--r--cbits/cryptonite_xsalsa.h37
-rwxr-xr-xci7
-rw-r--r--cryptonite-backport/Crypto/Cipher/Salsa.hs83
-rw-r--r--cryptonite-backport/Crypto/Cipher/XSalsa.hs50
-rw-r--r--cryptonite-backport/Crypto/ECC/Class.hs127
-rw-r--r--cryptonite-backport/Crypto/ECC/Simple/Prim.hs208
-rw-r--r--cryptonite-backport/Crypto/ECC/Simple/Types.hs615
-rw-r--r--cryptonite-backport/Crypto/Error/Types.hs106
-rw-r--r--cryptonite-backport/Crypto/Internal/ByteArray.hs19
-rw-r--r--cryptonite-backport/Crypto/Internal/Compat.hs48
-rw-r--r--cryptonite-backport/Crypto/Internal/DeepSeq.hs33
-rw-r--r--cryptonite-backport/Crypto/Internal/Imports.hs16
-rw-r--r--cryptonite-backport/Crypto/PubKey/Curve25519.hs131
-rw-r--r--dht-client.cabal213
-rw-r--r--examples/dht.hs90
-rw-r--r--examples/dhtd.hs1341
-rw-r--r--readpackets.hs82
-rw-r--r--src/Control/Concurrent/Async/Lifted/Instrument.hs5
-rw-r--r--src/Control/Concurrent/Lifted/Instrument.hs88
-rw-r--r--src/Control/Concurrent/Tasks.hs44
-rw-r--r--src/Control/TriadCommittee.hs89
-rw-r--r--src/Crypto/Tox.hs571
-rw-r--r--src/Data/BEncode/Pretty.hs83
-rw-r--r--src/Data/Bits/ByteString.hs132
-rw-r--r--src/Data/Digest/CRC32C.hs100
-rw-r--r--src/Data/MinMaxPSQ.hs99
-rw-r--r--src/Data/Torrent.hs1328
-rw-r--r--src/Data/Word64Map.hs62
-rw-r--r--src/Data/Wrapper/PSQ.hs81
-rw-r--r--src/Data/Wrapper/PSQInt.hs55
-rw-r--r--src/Network/Address.hs1225
-rw-r--r--src/Network/BitTorrent/DHT/ContactInfo.hs236
-rw-r--r--src/Network/BitTorrent/DHT/Readme.md13
-rw-r--r--src/Network/BitTorrent/DHT/Token.hs202
-rw-r--r--src/Network/BitTorrent/MainlineDHT.hs1111
-rw-r--r--src/Network/BitTorrent/MainlineDHT/Symbols.hs24
-rw-r--r--src/Network/Kademlia.hs180
-rw-r--r--src/Network/Kademlia/Bootstrap.hs432
-rw-r--r--src/Network/Kademlia/Routing.hs808
-rw-r--r--src/Network/Kademlia/Search.hs247
-rw-r--r--src/Network/QueryResponse.hs549
-rw-r--r--src/Network/SocketLike.hs124
-rw-r--r--src/Network/StreamServer.hs153
-rw-r--r--src/Network/Tox.hs369
-rw-r--r--src/Network/Tox/Crypto/Handlers.hs285
-rw-r--r--src/Network/Tox/Crypto/Transport.hs1172
-rw-r--r--src/Network/Tox/DHT/Handlers.hs432
-rw-r--r--src/Network/Tox/DHT/Transport.hs478
-rw-r--r--src/Network/Tox/NodeId.hs470
-rw-r--r--src/Network/Tox/Onion/Handlers.hs279
-rw-r--r--src/Network/Tox/Onion/Transport.hs927
-rw-r--r--src/Network/Tox/Transport.hs78
-rw-r--r--src/Network/UPNP.hs39
-rw-r--r--src/StaticAssert.hs13
-rw-r--r--src/System/Global6.hs28
-rw-r--r--src/Text/XXD.hs26
-rw-r--r--todo.txt54
-rwxr-xr-xvnet/build.sh89
-rwxr-xr-xvnet/clean.sh49
-rwxr-xr-xvnet/everywhere.sh8
-rwxr-xr-xvnet/mkroutes.sh19
-rwxr-xr-xvnet/run.sh45
-rwxr-xr-xvnet/screen-everywhere.sh21
-rwxr-xr-xvnet/screen-shell.sh14
-rwxr-xr-xvnet/show-links.sh2
162 files changed, 29695 insertions, 0 deletions
diff --git a/.ackrc b/.ackrc
new file mode 100644
index 00000000..dc0f9689
--- /dev/null
+++ b/.ackrc
@@ -0,0 +1,2 @@
1--ignore-dir=res
2--ignore-dir=sub
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..a8a88417
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
res/* binary
diff --git a/.gitignore b/.gitignore
index 6fabf46f..2c40503c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,18 @@
1nohup.out
2
3dist
4cabal-dev
5.cabal-sandbox
6cabal.sandbox.config
7tmp
8data
9upload-docs
10*.torrent
11*.aux
12*.eventlog
13*.hp
14*.pdf
15*.ps
16*.prof
17res/rtorrent-sessiondir
1/.stack-work/ 18/.stack-work/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.gitmodules
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000..55e6f926
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
1Sam Truzjan <pxqr.sta@gmail.com>
2Sam Truzjan <sta.cs.vsu@gmail.com>
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..3130bc52
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,35 @@
1language: haskell
2
3ghc:
4 - 7.6
5
6before_install:
7 - sudo apt-get install rtorrent screen
8 - rtorrent -h | grep version
9 - screen --version || true
10
11install:
12 - cabal sandbox init
13 - ./dev/update-dependencies.sh
14
15script:
16 - cabal configure && cabal build
17 - cabal configure -ftesting --enable-tests --enable-benchmark && cabal build && ./dist/build/spec/spec
18
19notifications:
20 email:
21 on_success: never
22 on_failure: change
23
24 irc:
25 channels:
26 - "chat.freenode.net#haskell-bittorrent"
27 on_success: change
28 on_failure: change
29
30 use_notice: true
31 template:
32 - "%{repository}#%{build_number} : %{message} for about %{duration}"
33 - "Commit: %{branch}/%{commit} by %{author}"
34 - "Changes: %{compare_url}"
35 - "Build: %{build_url}" \ No newline at end of file
diff --git a/Announcer.hs b/Announcer.hs
new file mode 100644
index 00000000..668e00c2
--- /dev/null
+++ b/Announcer.hs
@@ -0,0 +1,198 @@
1{-# LANGUAGE ExistentialQuantification #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3{-# LANGUAGE LambdaCase #-}
4{-# LANGUAGE NamedFieldPuns #-}
5{-# LANGUAGE NondecreasingIndentation #-}
6module Announcer
7 ( Announcer
8 , AnnounceKey
9 , packAnnounceKey
10 , unpackAnnounceKey
11 , AnnounceMethod(..)
12 , forkAnnouncer
13 , stopAnnouncer
14 , schedule
15 , cancel
16 ) where
17
18import qualified Data.MinMaxPSQ as MM
19import Data.Wrapper.PSQ as PSQ
20import InterruptibleDelay
21import Network.Kademlia.Routing as R
22import Network.Kademlia.Search
23
24import Control.Concurrent.Lifted.Instrument
25import Control.Concurrent.STM
26import Control.Monad
27import Data.ByteString (ByteString)
28import qualified Data.ByteString.Char8 as Char8
29import Data.Function
30import Data.Hashable
31import Data.Maybe
32import Data.Ord
33import Data.Time.Clock.POSIX
34
35newtype AnnounceKey = AnnounceKey ByteString
36 deriving (Hashable,Ord,Eq)
37
38packAnnounceKey :: Announcer -> String -> STM AnnounceKey
39packAnnounceKey _ = return . AnnounceKey . Char8.pack
40
41unpackAnnounceKey :: AnnounceKey -> AnnounceKey -> STM String
42unpackAnnounceKey _ (AnnounceKey bs) = return $ Char8.unpack bs
43
44data ScheduledItem
45 = StopAnnouncer
46 | NewAnnouncement (STM (IO ())) (IO ()) (IO ()) POSIXTime
47 | SearchFinished (IO ()) (IO ()) POSIXTime
48 | Announce (STM (IO ())) (IO ()) POSIXTime
49 | DeleteAnnouncement
50
51data Announcer = Announcer
52 { scheduled :: TVar (PSQ' AnnounceKey POSIXTime ScheduledItem)
53 , announcerActive :: TVar Bool
54 , interrutible :: InterruptibleDelay
55 }
56
57announceK :: Int
58announceK = 8
59
60data AnnounceState = forall nid addr tok ni r. AnnounceState
61 { aState :: SearchState nid addr tok ni r
62 , aStoringNodes :: TVar (MM.MinMaxPSQ ni (Down POSIXTime))
63 }
64
65scheduleImmediately :: Announcer -> AnnounceKey -> ScheduledItem -> STM ()
66scheduleImmediately announcer k item
67 = modifyTVar' (scheduled announcer) (PSQ.insert' k item 0)
68
69stopAnnouncer :: Announcer -> IO ()
70stopAnnouncer announcer = do
71 atomically $ scheduleImmediately announcer (AnnounceKey "*stop*") StopAnnouncer
72 interruptDelay (interrutible announcer)
73 atomically $ readTVar (announcerActive announcer) >>= check . not
74
75data AnnounceMethod r = forall nid ni sr addr tok a.
76 ( Show nid
77 , Hashable nid
78 , Hashable ni
79 , Ord addr
80 , Ord nid
81 , Ord ni
82 ) => AnnounceMethod
83 { aSearch :: Search nid addr tok ni sr
84 , aPublish :: r -> tok -> Maybe ni -> IO (Maybe a)
85 , aBuckets :: TVar (R.BucketList ni)
86 , aTarget :: nid
87 , aInterval :: POSIXTime
88 }
89
90schedule :: Announcer -> AnnounceKey -> AnnounceMethod r -> r -> IO ()
91schedule announcer k AnnounceMethod{aSearch,aPublish,aBuckets,aTarget,aInterval} r = do
92 st <- atomically $ newSearch aSearch aTarget []
93 ns <- atomically $ newTVar MM.empty
94 let astate = AnnounceState st ns
95 publishToNodes is = do
96 forM_ is $ \(Binding ni mtok _) -> do
97 forM_ mtok $ \tok -> do
98 got <- aPublish r tok (Just ni)
99 now <- getPOSIXTime
100 forM_ got $ \_ -> do
101 atomically $ modifyTVar ns $ MM.insertTake announceK ni (Down now)
102 announce = do -- publish to current search results
103 is <- atomically $ do
104 bs <- readTVar (searchInformant st {- :: TVar (MinMaxPSQ' ni nid tok -})
105 return $ MM.toList bs
106 publishToNodes is
107 onResult _ = return True -- action for each search-hit (True = keep searching)
108 searchAgain = searchIsFinished st >>= \isfin -> return $ when isfin $ void $ fork search
109 search = do -- thread to fork
110 atomically $ reset aBuckets aSearch aTarget st
111 searchLoop aSearch aTarget onResult st
112 fork $ do -- Announce to any nodes we haven't already announced to.
113 is <- atomically $ do
114 bs <- readTVar (searchInformant st {- :: TVar (MinMaxPSQ' ni nid tok -})
115 nq <- readTVar ns
116 return $ filter (\(Binding ni _ _) -> not $ isJust $ MM.lookup' ni nq)
117 $ MM.toList bs
118 publishToNodes is
119 return ()
120 {-
121 atomically $ scheduleImmediately announcer k
122 $ SearchFinished {- st -} search announce aInterval
123 interruptDelay (interrutible announcer)
124 -}
125 atomically $ scheduleImmediately announcer k $ NewAnnouncement searchAgain search announce aInterval
126 interruptDelay (interrutible announcer)
127
128cancel :: Announcer -> AnnounceKey -> AnnounceMethod r -> r -> IO ()
129cancel announcer k _ _ = do
130 atomically $ scheduleImmediately announcer k $ DeleteAnnouncement
131 interruptDelay (interrutible announcer)
132
133forkAnnouncer :: IO Announcer
134forkAnnouncer = do
135 delay <- interruptibleDelay
136 announcer <- atomically $ Announcer <$> newTVar PSQ.empty
137 <*> newTVar True
138 <*> pure delay
139 fork $ announceThread announcer
140 return announcer
141
142
143announceThread :: Announcer -> IO ()
144announceThread announcer = do
145 myThreadId >>= flip labelThread "announcer"
146 fix $ \loop -> do
147 join $ atomically $ do
148 item <- maybe retry return =<< findMin <$> readTVar (scheduled announcer)
149 return $ do
150 now <- getPOSIXTime
151 -- Is it time to do something?
152 if (prio item < now)
153 then do -- Yes. Dequeue and handle this event.
154 action <- atomically $ do
155 modifyTVar' (scheduled announcer)
156 (PSQ.delete (key item))
157 performScheduledItem announcer now item
158 -- Are we finished?
159 mapM_ (>> loop) -- No? Okay, perform scheduled op and loop.
160 action
161 else do -- No. Wait a bit.
162 startDelay (interrutible announcer) (microseconds $ prio item - now)
163 loop
164 -- We're done. Let 'stopAnnouncer' know that it can stop blocking.
165 atomically $ writeTVar (announcerActive announcer) False
166
167performScheduledItem :: Announcer -> POSIXTime -> Binding' AnnounceKey POSIXTime ScheduledItem -> STM (Maybe (IO ()))
168performScheduledItem announcer now = \case
169
170 (Binding _ StopAnnouncer _) -> return Nothing
171
172 -- announcement started:
173 (Binding k (NewAnnouncement checkFin search announce interval) _) -> do
174 modifyTVar (scheduled announcer)
175 (PSQ.insert' k (Announce checkFin announce interval) (now + interval))
176 return $ Just $ void $ fork search
177
178 -- announcement removed:
179 (Binding k DeleteAnnouncement _) -> return $ Just $ return ()
180
181 -- time for periodic announce:
182 -- (re-)announce to the current known set of storing-nodes.
183 -- TODO: If the search is finished, restart the search.
184 (Binding k (Announce checkFin announce interval) _) -> do
185 isfin <- checkFin
186 modifyTVar (scheduled announcer)
187 (PSQ.insert' k (Announce checkFin announce interval) (now + interval))
188 return $ Just $ do
189 isfin
190 announce
191
192 -- search finished:
193 -- if any of the current storing-nodes set have not been
194 -- announced to, announce to them.
195 (Binding _ (SearchFinished {- st -} search announce interval) _) -> return $ Just $ return ()
196
197
198
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 00000000..60a1006c
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,115 @@
12014-02-19 Sam Truzjan <pxqr.sta@gmail.com>
2
3 krpc 0.6.0.0
4
5 API changes:
6
7 * Added isActive: this predicate can be used to implement
8 MonadActive instance and useful for resource
9 initialization/finalization sanity check.
10
112014-01-08 Sam Truzjan <pxqr.sta@gmail.com>
12
13 krpc 0.6.0.0: Logging + exceptions.
14
15 API changes:
16
17 * MonadLogger is superclass of MonadKRPC;
18 * KError hidden from Network.KRPC;
19 * HandlerFailure added;
20 * QueryFailure and getQueryCount added.
21
222013-12-25 Sam Truzjan <pxqr.sta@gmail.com>
23
24 krpc 0.5.0.0: Major API changes.
25
26 * Added transaction handling;
27 * Use the same socket for server and client;
28 * New query function will infer query method from request/response
29 datatypes.
30 * Added MonadKRPC and KRPC classes.
31
322013-11-26 Sam Truzjan <pxqr.sta@gmail.com>
33
34 krpc
35
36 * 0.4.1.1: Fixed build failure on GHC == 7.4.*
37
382013-11-25 Sam Truzjan <pxqr.sta@gmail.com>
39
40 bittorrent
41
42 * Version 0.0.0.3
43 * use Pretty class from pretty-class package;
44 * Data.Torrent.Client.hs:
45 * /tests/: fixed;
46
472013-11-21 Sam Truzjan <pxqr.sta@gmail.com>
48
49 bittorrent
50
51 Version 0.0.0.2
52
53 * InfoHash.hs: added rendering to Text.
54 * Torrent.hs: added pretty printing and content type.
55 * Magnet.hs: added constructors from Torrent datatype.
56 * New: added Data.Torrent.Client, Data.Torrent.Progress,
57 Network.Bittorrent.Core.PeerId, Network.BitTorrent.PeerAddr
58 modules.
59
602013-11-01 Sam Truzjan <pxqr.sta@gmail.com>
61
62 bittorrent
63
64 Initial version: 0.0.0.1
65
662013-10-17 Sam Truzjan <pxqr.sta@gmail.com>
67
68 krpc
69
70 * 0.4.1.0: Use bencoding-0.4.*
71
722013-10-03 Sam Truzjan <pxqr.sta@gmail.com>
73
74 krpc
75
76 * 0.4.0.1: Minor documentation fixes.
77
782013-10-03 Sam Truzjan <pxqr.sta@gmail.com>
79
80 krpc
81
82 * 0.4.0.0: IPv6 support.
83
842013-09-28 Sam Truzjan <pxqr.sta@gmail.com>
85
86 krpc
87
88 * 0.3.0.0: Use bencoding-0.3.*
89 * Rename Remote.* to Network.* modules.
90
912013-09-28 Sam Truzjan <pxqr.sta@gmail.com>
92
93 krpc
94
95 * 0.2.2.0: Use bencoding-0.2.2.*
96
972013-08-27 Sam Truzjan <pxqr.sta@gmail.com>
98
99 krpc
100
101 * 0.2.0.0: Async API have been removed, use /async/ package
102 instead.
103 * Expose caller address in handlers.
104
1052013-07-09 Sam Truzjan <pxqr.sta@gmail.com>
106
107 krpc
108
109 * 0.1.1.0: Allow passing raw argument\/result dictionaries.
110
1112013-07-09 Sam Truzjan <pxqr.sta@gmail.com>
112
113 krpc
114
115 * 0.1.0.0: Initial version.
diff --git a/InterruptibleDelay.hs b/InterruptibleDelay.hs
new file mode 100644
index 00000000..d59ec8ef
--- /dev/null
+++ b/InterruptibleDelay.hs
@@ -0,0 +1,41 @@
1module InterruptibleDelay where
2
3import Control.Concurrent
4import Control.Monad
5import Control.Exception ({-evaluate,-}handle,ErrorCall(..))
6import Data.Time.Clock (NominalDiffTime)
7
8type Microseconds = Int
9
10microseconds :: NominalDiffTime -> Microseconds
11microseconds d = round $ 1000000 * d
12
13data InterruptibleDelay = InterruptibleDelay
14 { delayThread :: MVar ThreadId
15 }
16
17interruptibleDelay :: IO InterruptibleDelay
18interruptibleDelay = do
19 fmap InterruptibleDelay newEmptyMVar
20
21startDelay :: InterruptibleDelay -> Microseconds -> IO Bool
22startDelay d interval = do
23 thread <- myThreadId
24 handle (\(ErrorCall _)-> do
25 debugNoise $ "delay interrupted"
26 return False) $ do
27 putMVar (delayThread d) thread
28 threadDelay interval
29 void $ takeMVar (delayThread d)
30 return True
31
32 where debugNoise str = return ()
33
34
35interruptDelay :: InterruptibleDelay -> IO ()
36interruptDelay d = do
37 mthread <- do
38 tryTakeMVar (delayThread d)
39 flip (maybe $ return ()) mthread $ \thread -> do
40 throwTo thread (ErrorCall "Interrupted delay")
41
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..4c30139e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
1Copyright (c) 2013, Sam Truzjan
2
3All rights reserved.
4
5Redistribution and use in source and binary forms, with or without
6modification, are permitted provided that the following conditions are met:
7
8 * Redistributions of source code must retain the above copyright
9 notice, this list of conditions and the following disclaimer.
10
11 * Redistributions in binary form must reproduce the above
12 copyright notice, this list of conditions and the following
13 disclaimer in the documentation and/or other materials provided
14 with the distribution.
15
16 * Neither the name of Sam Truzjan nor the names of other
17 contributors may be used to endorse or promote products derived
18 from this software without specific prior written permission.
19
20THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..e2eba85f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,6 @@
1.PHONY: clean
2
3clean:
4 rm -rf tmp
5 rm *.aux *.eventlog *.ps *.hp *.pdf
6 cabal clean \ No newline at end of file
diff --git a/OnionRouter.hs b/OnionRouter.hs
new file mode 100644
index 00000000..20279c5d
--- /dev/null
+++ b/OnionRouter.hs
@@ -0,0 +1,451 @@
1{-# LANGUAGE NondecreasingIndentation #-}
2{-# LANGUAGE LambdaCase #-}
3module OnionRouter where
4
5import Control.Concurrent.Lifted.Instrument
6import Crypto.Tox
7import Network.Address
8import Network.Kademlia
9import Network.Kademlia.Routing
10import Network.QueryResponse
11import Network.Tox.NodeId
12import Network.Tox.Onion.Transport
13
14import Control.Arrow
15import Control.Concurrent.STM
16import Control.Monad
17import Crypto.PubKey.Curve25519
18import Crypto.Random
19import Data.Bits
20import qualified Data.ByteString as B
21import Data.Hashable
22import qualified Data.HashMap.Strict as HashMap
23 ;import Data.HashMap.Strict (HashMap)
24import qualified Data.IntMap as IntMap
25 ;import Data.IntMap (IntMap)
26import Data.Maybe
27import qualified Data.Serialize as S
28import Data.Typeable
29import Data.Word
30import qualified Data.Word64Map as W64
31 ;import Data.Word64Map (Word64Map, fitsInInt)
32import Network.Socket
33import System.Endian
34import System.Timeout
35
36-- Toxcore saves a maximum of 12 paths: 6 paths are reserved for announcing
37-- ourselves and 6 others are used to search for friends.
38--
39-- Note: This is pointless because a man-in-the-middle attack currently makes
40-- it trivial to glean friend relationships: the storing node can swap the
41-- published to-route key with his own giving him access to one layer of
42-- encryption and thus the real public key of the sender. TODO:
43-- Counter-measures.
44--
45-- Unlike toxcore, we don't currently reserve paths for only-searching or
46-- only-announcing. Instead, we maintain 12 multi-purpose routes.
47data OnionRouter = OnionRouter
48 { -- | For every query, we remember the destination and source keys
49 -- so we can decrypt the response. Note, the RouteId field is not
50 -- currently stored here. It is inferred from the destination NodeId.
51 -- Instead, a 'Nothing' is stored.
52 pendingQueries :: TVar (Word64Map (OnionDestination RouteId))
53 -- | The current 12 routes that may be assigned to outgoing packets.
54 , routeMap :: TVar (IntMap RouteRecord)
55 -- | A set of nodes used to query for random route nodes. These aren't
56 -- used directly in onion routes, they are queried for route nodes that
57 -- are nearby randomly selected ids.
58 --
59 -- These nodes are chosen from the kademlia buckets and when one of them
60 -- is evicted from a bucket, it is no longer used as a trampoline node.
61 --
62 -- Also, currently our own address is (unnecessarily) stored here at
63 -- index (-1).
64 , trampolineNodes :: TVar (IntMap NodeInfo)
65 -- | This map associates 'NodeId' values with the corresponding
66 -- 'trampolineNodes' index.
67 , trampolineIds :: TVar (HashMap NodeId Int)
68 -- | Indicates the current size of 'trampolineNodes'.
69 , trampolineCount :: TVar Int
70 -- | The pseudo-random generator used to select onion routes.
71 , onionDRG :: TVar ChaChaDRG
72 -- | Building onion routes happens in a dedicated thread. See 'forkRouteBuilder'.
73 , routeThread :: ThreadId
74 -- | Each of the 12 routes has a flag here that is set True when the
75 -- route should be discarded and replaced with a fresh one.
76 , pendingRoutes :: IntMap (TVar Bool)
77 -- | Debug prints are written to this channel which is then flushed to
78 -- 'routeLogger'.
79 , routeLog :: TChan String
80 -- | User supplied log function.
81 , routeLogger :: String -> IO ()
82 }
83
84data RouteRecord = RouteRecord
85 { storedRoute :: OnionRoute
86 , responseCount :: !Int
87 , timeoutCount :: !Int
88 }
89
90-- Onion paths have different timeouts depending on whether the path is
91-- confirmed or unconfirmed. Unconfirmed paths (paths that core has never
92-- received any responses from) have a timeout of 4 seconds with 2 tries before
93-- they are deemed non working. This is because, due to network conditions,
94-- there may be a large number of newly created paths that do not work and so
95-- trying them a lot would make finding a working path take much longer. The
96-- timeout for a confirmed path (from which a response was received) is 10
97-- seconds with 4 tries without a response. A confirmed path has a maximum
98-- lifetime of 1200 seconds to make possible deanonimization attacks more
99-- difficult.
100timeoutForRoute :: RouteRecord -> Int
101timeoutForRoute RouteRecord{ responseCount = 0 } = 4000000
102timeoutForRoute RouteRecord{ responseCount = _ } = 10000000
103
104freshRoute :: OnionRoute -> RouteRecord
105freshRoute r = RouteRecord
106 { storedRoute = r
107 , responseCount = 0
108 , timeoutCount = 0
109 }
110
111gotResponse :: RouteRecord -> RouteRecord
112gotResponse rr = rr
113 { responseCount = succ $ responseCount rr
114 , timeoutCount = 0
115 }
116
117gotTimeout :: RouteRecord -> RouteRecord
118gotTimeout rr = rr
119 { timeoutCount = succ $ timeoutCount rr
120 }
121
122data RouteEvent = BuildRoute RouteId
123
124newOnionRouter :: (String -> IO ()) -> IO OnionRouter
125newOnionRouter perror = do
126 drg0 <- drgNew
127 or <- atomically $ do
128 chan <- newTChan
129 drg <- newTVar drg0
130 forM_ [0..11] $ \n -> writeTChan chan $ BuildRoute (RouteId n)
131 pq <- newTVar W64.empty
132 rm <- newTVar IntMap.empty
133 tn <- newTVar IntMap.empty
134 ti <- newTVar HashMap.empty
135 tc <- newTVar 0
136 vs <- sequence $ replicate 12 (newTVar True)
137 rlog <- newTChan
138 return OnionRouter
139 { pendingRoutes = IntMap.fromList $ zip [0..11] vs
140 , onionDRG = drg
141 , pendingQueries = pq
142 , routeMap = rm
143 , trampolineNodes = tn
144 , trampolineIds = ti
145 , trampolineCount = tc
146 , routeLog = rlog
147 , routeThread = error "Failed to invoke forkRouteBuilder"
148 , routeLogger = perror
149 }
150 return or
151
152forkRouteBuilder :: OnionRouter -> (NodeId -> NodeInfo -> IO (Maybe [NodeInfo])) -> IO OnionRouter
153forkRouteBuilder or getnodes = do
154 tid <- forkIO $ do
155 me <- myThreadId
156 labelThread me "OnionRouter"
157 forever $ do
158 let checkRebuild rid want_build stm = flip orElse stm $ do
159 readTVar want_build >>= check
160 -- This was moved to handleEvent to allow retry on fail.
161 -- writeTVar want_build False -- Prevent redundant BuildRoute events.
162 return $ BuildRoute $ RouteId rid
163 io <- atomically $
164 (readTChan (routeLog or) >>= return . routeLogger or)
165 `orElse`
166 (IntMap.foldrWithKey checkRebuild retry (pendingRoutes or)
167 >>= return . handleEvent getnodes or { routeThread = me })
168 io
169 return or { routeThread = tid }
170
171generateNodeId :: MonadRandom m => m NodeId
172generateNodeId = either (error "unable to make random nodeid")
173 id
174 . S.decode <$> getRandomBytes 32
175
176distinct3by :: Eq t => (a -> t) -> a -> a -> a -> Bool
177distinct3by f a b c = f a /= f b && f b /= f c && f c /= f a
178
179-- The two integer functions below take an [inclusive,inclusive] range.
180randomR :: (DRG g, Integral a) => (a, a) -> g -> (a, g)
181randomR (l,h) = randomIvalInteger (toInteger l, toInteger h)
182
183next :: DRG g => g -> (Int,g)
184next g = withDRG g $ do bs <- getRandomBytes $ if fitsInInt (Proxy :: Proxy Word64)
185 then 8
186 else 4
187 either (return . error) return $ S.decode bs
188
189randomIvalInteger :: (DRG g, Num a) => (Integer, Integer) -> g -> (a, g)
190randomIvalInteger (l,h) rng
191 | l > h = randomIvalInteger (h,l) rng
192 | otherwise = case (f 1 0 rng) of (v, rng') -> (fromInteger (l + v `mod` k), rng')
193 where
194 (genlo, genhi) = (minBound :: Int, maxBound :: Int) -- genRange :: RandomGen g => g -> (Int, Int)
195 b = fromIntegral genhi - fromIntegral genlo + 1
196
197 -- Probabilities of the most likely and least likely result
198 -- will differ at most by a factor of (1 +- 1/q). Assuming the RandomGen
199 -- is uniform, of course
200
201 -- On average, log q / log b more random values will be generated
202 -- than the minimum
203 q = 1000
204 k = h - l + 1
205 magtgt = k * q
206
207 -- generate random values until we exceed the target magnitude
208 f mag v g | mag >= magtgt = (v, g)
209 | otherwise = v' `seq`f (mag*b) v' g' where
210 (x,g') = next g -- next :: RandomGen g => g -> (Int, g)
211 v' = (v * b + (fromIntegral x - fromIntegral genlo))
212
213selectTrampolines :: OnionRouter -> IO [NodeInfo]
214selectTrampolines or = do
215 myThreadId >>= flip labelThread ("OnionRouter.selectTrampolines")
216 atomically (selectTrampolines' or) >>= \case
217 Left ns -> do
218 -- atomically $ writeTChan (routeLog or)
219 routeLogger or $ unwords
220 ( "ONION Discarding insecure trampolines:" : (map (show . nodeAddr) ns) )
221 myThreadId >>= flip labelThread ("OnionRouter.selectTrampolines.sleep")
222 threadDelay 1000000
223 myThreadId >>= flip labelThread ("OnionRouter.selectTrampolines")
224 selectTrampolines or
225 Right ns -> do
226 myThreadId >>= flip labelThread ("OnionRouter")
227 return ns
228
229selectTrampolines' :: OnionRouter -> STM (Either [NodeInfo] [NodeInfo])
230selectTrampolines' or = do
231 cnt <- readTVar (trampolineCount or)
232 ts <- readTVar (trampolineNodes or)
233 drg0 <- readTVar (onionDRG or)
234 let (a, drg1) = randomR (0,cnt - 1) drg0
235 (b0, drg2) = randomR (0,cnt - 2) drg1
236 (c0, drg ) = randomR (0,cnt - 3) drg2
237 b | b0 < a = b0
238 | otherwise = b0 + 1
239 c1 | c0 < a = c0
240 | otherwise = c0 + 1
241 c | c1 < b = c1
242 | otherwise = c1 + 1
243 ns = mapMaybe (\n -> IntMap.lookup n ts) [a,b,c]
244 ns' <- case ns of
245 [an,bn,cn] | distinct3by nodeClass an bn cn
246 -> return $ Right ns
247 _ -> return $ Left ns
248 writeTVar (onionDRG or) drg
249 return ns'
250
251handleEvent :: (NodeId -> NodeInfo -> IO (Maybe [NodeInfo])) -> OnionRouter -> RouteEvent -> IO ()
252handleEvent getnodes or e@(BuildRoute (RouteId rid)) = do
253 routeLogger or $ "ONION Rebuilding RouteId " ++ show rid
254 mb <- do
255 ts <- selectTrampolines or
256 join . atomically $ do
257 drg <- readTVar (onionDRG or)
258 [av,bv,cv] <- sequence $ replicate 3 (newTVar Nothing)
259 let (getr, drg') = withDRG drg $ do
260 asec <- generateSecretKey -- Three aliases
261 bsec <- generateSecretKey
262 csec <- generateSecretKey
263 aq <- generateNodeId -- Three queries
264 bq <- generateNodeId
265 cq <- generateNodeId
266 sel <- B.head <$> getRandomBytes 1 -- Three two-bit result selectors (6 bits)
267 let asel = sel .&. 0x3
268 bsel = shiftR sel 2 .&. 0x3
269 csel = shiftR sel 4 .&. 0x3
270 sendq s q ni = fmap (listToMaybe . drop (fromIntegral s)) <$> getnodes q ni
271 sendqs = do
272 forkIO $ sendq asel aq (ts !! 0) >>= atomically . writeTVar av . Just
273 forkIO $ sendq bsel bq (ts !! 1) >>= atomically . writeTVar bv . Just
274 forkIO $ sendq csel cq (ts !! 2) >>= atomically . writeTVar cv . Just
275 -- This timeout should be unnecessary... But I'm paranoid.
276 -- Note: 10 seconds should be sufficient for typical get-nodes queries.
277 tm <- timeout 20000000 $ atomically $ do -- Wait for all 3 results.
278 rs <- catMaybes <$> sequence [readTVar av,readTVar bv,readTVar cv]
279 case rs of [_,_,_] -> do
280 return $ catMaybes $ catMaybes rs
281 -- self <- IntMap.lookup (-1) <$> readTVar (trampolineNodes or)
282 -- return $ maybe (catMaybes rs) (\x -> [x,x,x]) self
283 _ -> retry
284 maybe (routeLogger or "ONION: Unexpected sendq timeout!" >> return [])
285 return
286 tm
287 return $ do
288 myThreadId >>= flip labelThread ("OnionRouter.sendqs")
289 nodes <- case ts of
290 [_,_,_] -> sendqs
291 _ -> return []
292 myThreadId >>= flip labelThread ("OnionRouter")
293 routeLogger or $ unlines
294 [ "ONION trampolines: " ++ show ts
295 , "ONION query results: " ++ show nodes ]
296 case nodes of
297 [a,b,c] | distinct3by nodeClass a b c -> do
298 atomically $ do
299 writeTChan (routeLog or) $ unwords [ "ONION using route:"
300 , show $ nodeAddr a
301 , show $ nodeAddr b
302 , show $ nodeAddr c ]
303 return $ Just OnionRoute
304 { routeAliasA = asec
305 , routeAliasB = bsec
306 , routeAliasC = csec
307 , routeNodeA = a
308 , routeNodeB = b
309 , routeNodeC = c
310 }
311 [a,b,c] -> do
312 atomically $ writeTChan (routeLog or) $ unwords [ "ONION Discarding insecure route:", show $ nodeAddr a, show $ nodeAddr b, show $ nodeAddr c]
313 return Nothing
314 _ -> return Nothing
315 writeTVar (onionDRG or) drg'
316 return $ getr
317 atomically $ maybe (writeTVar (pendingRoutes or IntMap.! rid) True)
318 (\r -> do modifyTVar' (routeMap or)
319 (IntMap.insert rid $ freshRoute r)
320 writeTVar (pendingRoutes or IntMap.! rid) False
321 )
322 mb
323 case mb of
324 Just _ -> routeLogger or $ "ONION Finished RouteId " ++ show rid
325 Nothing -> routeLogger or $ "ONION Failed RouteId " ++ show rid
326
327lookupSender :: OnionRouter -> SockAddr -> Nonce8 -> IO (Maybe (OnionDestination RouteId))
328lookupSender or saddr (Nonce8 w8) = do
329 result <- atomically $ do
330 ks <- readTVar (pendingQueries or)
331 let r = W64.lookup w8 ks
332 writeTChan (routeLog or) $ "ONION lookupSender " ++ unwords [show w8, "->", show r]
333 return r
334 return $ do
335 od <- result
336 let nid = nodeId $ onionNodeInfo od
337 ni <- either (const Nothing) Just $ nodeInfo nid saddr
338 Just (OnionDestination (onionAliasSelector od)
339 ni
340 (Just $ routeId nid))
341
342lookupRoute :: OnionRouter -> NodeInfo -> RouteId -> IO (Maybe OnionRoute)
343lookupRoute or ni (RouteId rid) = do
344 mb <- atomically $ IntMap.lookup rid <$> readTVar (routeMap or)
345 return $ storedRoute <$> mb
346
347lookupTimeout :: OnionRouter -> Nonce8 -> OnionDestination r -> STM (OnionDestination RouteId, Int)
348lookupTimeout or n8 (OnionDestination asel ni Nothing) = do
349 let RouteId rid = routeId (nodeId ni)
350 mrr <- IntMap.lookup rid <$> readTVar (routeMap or)
351 readTVar (routeMap or) >>= \rm -> writeTChan (routeLog or) $ "ONION lookupTimeout " ++ unwords [show rid,show (IntMap.keys rm)]
352 case mrr of
353 Just rr -> return ( OnionDestination asel ni (Just $ RouteId rid), timeoutForRoute rr)
354 Nothing -> return ( OnionDestination asel ni Nothing , 0 )
355
356hookQueries :: OnionRouter -> (tid -> Nonce8)
357 -> TransactionMethods d tid (OnionDestination r) x
358 -> TransactionMethods d tid (OnionDestination r) x
359hookQueries or t8 tmethods = TransactionMethods
360 { dispatchRegister = \mvar od d -> do -- :: MVar x -> d -> STM (tid, d)
361 (tid,d') <- dispatchRegister tmethods mvar od d
362 let Nonce8 w8 = t8 tid
363 od' = case od of OnionDestination {} -> od { onionRouteSpec = Nothing }
364 OnionToOwner a b -> OnionToOwner a b -- Type cast.
365 ni = onionNodeInfo od
366 modifyTVar' (pendingQueries or) (W64.insert w8 od')
367 writeTChan (routeLog or) $ "ONION query add " ++ unwords [ show w8, ":=", show ni ]
368 return (tid,d')
369 , dispatchResponse = \tid x d -> do -- :: tid -> x -> d -> STM (d, IO ())
370 let Nonce8 w8 = t8 tid
371 mb <- W64.lookup w8 <$> readTVar (pendingQueries or)
372 modifyTVar' (pendingQueries or) (W64.delete w8)
373 forM_ mb $ \od -> do
374 let RouteId rid = routeId (nodeId (onionNodeInfo od))
375 modifyTVar' (routeMap or) (IntMap.adjust gotResponse rid)
376 writeTChan (routeLog or) $ "ONION query del " ++ show w8
377 dispatchResponse tmethods tid x d
378 , dispatchCancel = \tid d -> do -- :: tid -> d -> STM d
379 let Nonce8 w8 = t8 tid
380 mb <- W64.lookup w8 <$> readTVar (pendingQueries or)
381 modifyTVar' (pendingQueries or) (W64.delete w8)
382 forM_ mb $ \od -> do
383 let RouteId rid = routeId (nodeId (onionNodeInfo od))
384 modifyTVar' (routeMap or) (IntMap.adjust gotTimeout rid)
385 let expireRoute = writeTVar (pendingRoutes or IntMap.! rid) True
386 rr <- IntMap.lookup rid <$> readTVar (routeMap or)
387 case rr of
388 Just RouteRecord{ responseCount=0
389 , timeoutCount = c } | c >= 2 -> expireRoute
390 Just RouteRecord{ timeoutCount = c } | c >= 4 -> expireRoute
391 _ -> return ()
392 writeTChan (routeLog or) $ "ONION query can " ++ show w8
393 dispatchCancel tmethods tid d
394 }
395
396
397hookBucketList :: KademliaSpace NodeId NodeInfo -> TVar (BucketList NodeInfo) -> OnionRouter -> RoutingTransition NodeInfo -> STM ()
398hookBucketList kademlia bkts0 or (RoutingTransition ni Accepted) = do
399 s <- do
400 drg0 <- readTVar (onionDRG or)
401 bkts <- readTVar bkts0
402 let antibias = 2 ^ bucketNumber kademlia (nodeId ni) bkts
403 (s,drg) = randomR (0,antibias - 1) drg0
404 writeTVar (onionDRG or) drg
405 do -- Store localhost as trampoline node (-1).
406 -- This is not used, but harmless. I'm leaving it in for
407 -- testing purposes.
408 let self = (thisNode bkts) { nodeIP = read "127.0.0.1" }
409 modifyTVar' (trampolineNodes or) (IntMap.insert (-1) self)
410 return s
411 -- debias via stochastic filter
412 when (s == 0) $ do
413 ns <- readTVar (trampolineIds or)
414 case HashMap.lookup (nodeId ni) ns of
415 Just _ -> return ()
416 Nothing -> do
417 cnt <- readTVar (trampolineCount or)
418 writeTChan (routeLog or) $ "ONION trampoline Accepted " ++ unwords [show cnt, show ni]
419 modifyTVar' (trampolineIds or) (HashMap.insert (nodeId ni) cnt)
420 modifyTVar' (trampolineNodes or) (IntMap.insert cnt ni)
421 writeTVar (trampolineCount or) (succ cnt)
422hookBucketList _ _ or (RoutingTransition ni Stranger) = do
423 ns <- readTVar (trampolineIds or)
424 case HashMap.lookup (nodeId ni) ns of
425 Just n -> do writeTVar (trampolineIds or) (HashMap.delete (nodeId ni) ns)
426 cnt <- pred <$> readTVar (trampolineCount or)
427 writeTVar (trampolineCount or) cnt
428 if n == cnt
429 then modifyTVar' (trampolineNodes or) (IntMap.delete n)
430 else do lastnode <- (IntMap.! cnt) <$> readTVar (trampolineNodes or)
431 modifyTVar' (trampolineNodes or) (IntMap.insert n lastnode . IntMap.delete cnt)
432 writeTChan (routeLog or) $ "ONION trampoline Stranger " ++ unwords [show n,show ni]
433 Nothing -> return ()
434hookBucketList _ _ _ _ = return () -- ignore Applicant event.
435
436newtype IPClass = IPClass Word32
437 deriving Eq
438
439ipkey :: IPClass -> Int
440ipkey (IPClass k) = fromIntegral k
441
442nodeClass :: NodeInfo -> IPClass
443nodeClass = ipClass. nodeAddr
444
445ipClass :: SockAddr -> IPClass
446ipClass= either ipClass' ipClass' . either4or6
447
448ipClass' :: SockAddr -> IPClass
449ipClass' (SockAddrInet _ addr) = IPClass (fromBE32 addr .&. 0xFF000000)
450ipClass' (SockAddrInet6 _ _ (hiword,_,_,_) _) = IPClass hiword
451ipClass' _ = IPClass 0 -- unreachable.
diff --git a/Roster.hs b/Roster.hs
new file mode 100644
index 00000000..7c40e371
--- /dev/null
+++ b/Roster.hs
@@ -0,0 +1,100 @@
1{-# LANGUAGE NamedFieldPuns #-}
2module Roster where
3
4import Control.Concurrent.STM
5import Control.Monad
6import Crypto.PubKey.Curve25519
7import qualified Data.HashMap.Strict as HashMap
8 ;import Data.HashMap.Strict (HashMap)
9import Data.Maybe
10import Network.Tox.DHT.Transport as DHT
11import Network.Tox.NodeId
12import Network.Tox.Onion.Transport as Onion
13import System.IO
14
15newtype Roster = Roster { accounts :: TVar (HashMap NodeId Account) }
16
17data Account = Account
18 { userSecret :: SecretKey -- local secret key
19 , contacts :: TVar (HashMap NodeId Contact) -- received contact info
20 }
21
22data Contact = Contact
23 { contactKeyPacket :: Maybe (DHT.DHTPublicKey)
24 , contactFriendRequest :: Maybe (DHT.FriendRequest)
25 }
26
27mergeContact :: Contact -> Maybe Contact -> Maybe Contact
28mergeContact (Contact newk newf) (Just (Contact oldk oldf)) =
29 Just (Contact mergek mergef)
30 where
31 mergek = mplus oldk $ do
32 n <- newk
33 stamp <- fmap DHT.dhtpkNonce oldk `mplus` return minBound
34 guard (stamp <= DHT.dhtpkNonce n)
35 return n
36 mergef = mplus oldf newf
37mergeContact new Nothing = Just new
38
39newRoster :: IO Roster
40newRoster = atomically $ Roster <$> newTVar HashMap.empty
41
42newAccount :: SecretKey -> STM Account
43newAccount sk = Account sk <$> newTVar HashMap.empty
44
45addRoster :: Roster -> SecretKey -> STM ()
46addRoster (Roster as) sk = do
47 a <- newAccount sk
48 modifyTVar' as $ HashMap.insert (key2id $ toPublic sk) a
49
50delRoster :: Roster -> PublicKey -> STM ()
51delRoster (Roster as) pk = modifyTVar' as $ HashMap.delete (key2id pk)
52
53updateRoster :: Roster -> Onion.AnnouncedRendezvous -> (PublicKey,Onion.OnionData) -> IO ()
54updateRoster roster Onion.AnnouncedRendezvous{remoteUserKey} (localUserKey,omsg) = do
55 hPutStrLn stderr "updateRoster!!!"
56 atomically $ do
57 as <- readTVar (accounts roster)
58 maybe (return ())
59 (updateAccount remoteUserKey omsg)
60 $ HashMap.lookup (key2id localUserKey) as
61
62
63updateAccount :: PublicKey -> Onion.OnionData -> Account -> STM ()
64updateAccount remoteUserKey (Onion.OnionDHTPublicKey dhtpk) acc = do
65 modifyTVar' (contacts acc) $ HashMap.alter (mergeContact $ Contact (Just dhtpk) Nothing)
66 (key2id remoteUserKey)
67
68updateAccount remoteUserKey (Onion.OnionFriendRequest fr) acc = do
69 modifyTVar' (contacts acc) $ HashMap.alter (mergeContact $ Contact Nothing (Just fr))
70 (key2id remoteUserKey)
71
72dnsPresentation :: Roster -> STM String
73dnsPresentation (Roster accsvar) = do
74 accs <- readTVar accsvar
75 ms <- forM accs $ \Account { userSecret = sec, contacts = cvar } -> do
76 cs <- readTVar cvar
77 return $
78 "; local key = " ++ show (key2id $ toPublic sec) ++ "\n"
79 ++ concatMap dnsPresentation1
80 (mapMaybe (\(nid,m) -> ((,) nid) <$> contactKeyPacket m)
81 $ HashMap.toList cs)
82 return $ concat ms
83
84dnsPresentation1 :: (NodeId,DHTPublicKey) -> String
85dnsPresentation1 (nid,dk) = unlines
86 [ concat [ show nid, ".tox. IN CNAME ", show (key2id $ dhtpk dk), ".dht." ]
87 ]
88
89type LocalKey = NodeId
90type RemoteKey = NodeId
91
92friendRequests :: Roster -> STM (HashMap LocalKey [(RemoteKey,DHT.FriendRequest)])
93friendRequests (Roster roster) = do
94 accs <- readTVar roster
95 forM accs $ \Account { userSecret = sec, contacts = cvar } -> do
96 cs <- readTVar cvar
97 let remotes = mapMaybe (\(nid,m) -> ((,) nid) <$> contactFriendRequest m)
98 $ HashMap.toList cs
99 return remotes
100
diff --git a/TODO.org b/TODO.org
new file mode 100644
index 00000000..dbba5c8c
--- /dev/null
+++ b/TODO.org
@@ -0,0 +1,17 @@
1* configure travis
2* liftKRPC ::
3* add withRetries
4* bump version to 0.7.0.0
5
6* add issue: getQueryCount --> getRpcStats
7data Stats = Stats
8 { queryFailed :: {-# UNPACK #-} !Int
9 , querySucceed :: {-# UNPACK #-} !Int
10 , handlerFailed :: {-# UNPACK #-} !Int
11 , handlerSucceed :: {-# UNPACK #-} !Int
12 , sentBytes :: {-# UNPACK #-} !Int
13 , receivedBytes :: {-# UNPACK #-} !Int
14 }
15
16* add asyncQuery :: SockAddr -> a -> m (Async a)
17* add queries :: [(SockAddr, a)] -> m [Either a] \ No newline at end of file
diff --git a/bittorrent/README.md b/bittorrent/README.md
new file mode 100644
index 00000000..32948896
--- /dev/null
+++ b/bittorrent/README.md
@@ -0,0 +1,78 @@
1### BitTorrent [![Build Status][1]][2]
2
3A [BitTorrent][0] library implementation. It allows to read/write
4torrent files, transfer data files, query trackers and DHT. The
5library is still in active development and have some subsystems
6partially implemented.
7
8For lastest released version and reference documentation see [hackage][3] page.
9
10[0]: http://bittorrent.org/beps/bep_0000.html
11[1]: https://travis-ci.org/cobit/bittorrent.png
12[2]: https://travis-ci.org/cobit/bittorrent
13[3]: http://hackage.haskell.org/package/bittorrent
14
15### Status
16
17The protocol has [many enchancements][bep-list]. This table keep track
18if a particular BEP is "todo", "in progress" or "complete":
19
20| BEP # | Title | Status
21|:-----:|:--------------------------------------------------:|:-----------
22| 3 | [The BitTorrent Protocol Specification][bep3] | [In progress][bep3-impl]
23| 4 | [Known Number Allocations][bep4] | [In progress][bep4-impl]
24| 5 | [DHT][bep5] | [In progress][bep5-impl]
25| 6 | [Fast Extension][bep6] | [In progress][bep6-impl]
26| 7 | [IPv6 Tracker Extension][bep7] | [In progress][bep7-impl]
27| 9 | [Extension for Peers to Send Metadata Files][bep9] | [In progress][bep9-impl]
28| 10 | [Extension protocol][bep10] | [In progress][bep10-impl]
29| 12 | [Multitracker Metadata Extension][bep10] | [In progress][bep12-impl]
30| 15 | [UDP Tracker Protocol for BitTorrent][bep15] | [In progress][bep15-impl]
31| 20 | [Peer ID Conventions][bep20] | [Implemented][bep20-impl]
32| 23 | [Tracker Return Compact Peer Lists][bep23] | [Implemented][bep23-impl]
33
34[bep-list]: http://www.bittorrent.org/beps/bep_0000.html
35[bep3]: http://www.bittorrent.org/beps/bep_0003.html
36[bep4]: http://www.bittorrent.org/beps/bep_0004.html
37[bep5]: http://www.bittorrent.org/beps/bep_0005.html
38[bep6]: http://www.bittorrent.org/beps/bep_0006.html
39[bep7]: http://www.bittorrent.org/beps/bep_0007.html
40[bep9]: http://www.bittorrent.org/beps/bep_0009.html
41[bep10]: http://www.bittorrent.org/beps/bep_0010.html
42[bep12]: http://www.bittorrent.org/beps/bep_0012.html
43[bep15]: http://www.bittorrent.org/beps/bep_0015.html
44[bep20]: http://www.bittorrent.org/beps/bep_0020.html
45[bep23]: http://www.bittorrent.org/beps/bep_0023.html
46
47[bep3-impl]: src
48[bep4-impl]: src/Network/BitTorrent/Exchange/Message.hs
49[bep5-impl]: src/Network/BitTorrent/DHT/Protocol.hs
50[bep6-impl]: src/Network/BitTorrent/Exchange/Message.hs
51[bep7-impl]: src/Network/BitTorrent/Tracker/Message.hs
52[bep9-impl]: src/Network/BitTorrent/Exchange/Wire.hs
53[bep10-impl]: src/Network/BitTorrent/Exchange/Message.hs
54[bep12-impl]: src/Data/Torrent.hs
55[bep15-impl]: src/Network/BitTorrent/Tracker/RPC/UDP.hs
56[bep20-impl]: src/Network/BitTorrent/Core/Fingerprint.hs
57[bep23-impl]: src/Network/BitTorrent/Tracker/Message.hs
58
59### Hacking
60
61The root directory layout is as follows:
62
63* examples -- includes demo utilities to get started;
64* src -- the library source tree;
65* tests -- the library test suite;
66* res -- torrents and data files used in test suite.
67* sub -- subprojects and submodules used by the library and still in dev.
68
69Some subdirectories includes README with futher explanations to get started.
70
71### Contacts
72
73* Discussions: IRC [#haskell-bittorrent][irc] at irc.freenode.net
74* Bugs & issues: [issue tracker][tracker]
75* Maintainer: <pxqr.sta@gmail.com>
76
77[tracker]: https://github.com/cobit/bittorrent/issues/new
78[irc]: http://webchat.freenode.net/?channels=haskell-bittorrent
diff --git a/bittorrent/Readme.md b/bittorrent/Readme.md
new file mode 100644
index 00000000..e092c3ad
--- /dev/null
+++ b/bittorrent/Readme.md
@@ -0,0 +1,8 @@
1Layout
2======
3
4| module group | can import | main purpose |
5|:-------------|:----------------:|:-----------------------:|
6| /Network | /Data & /System | peer and data exchange |
7| /System | /Data | filesystem interface |
8| /Data | | torrent metadata |
diff --git a/bittorrent/bench/Main.hs b/bittorrent/bench/Main.hs
new file mode 100644
index 00000000..f04485ab
--- /dev/null
+++ b/bittorrent/bench/Main.hs
@@ -0,0 +1,75 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE MultiParamTypeClasses #-}
3{-# OPTIONS -fno-warn-orphans #-}
4module Main (main) where
5
6import Control.DeepSeq
7import Network
8import Control.Monad
9import Control.Monad.Logger
10import Control.Monad.Reader
11import Criterion.Main
12import Data.ByteString as BS
13import Network.DatagramServer
14
15
16import Network.BitTorrent.Exchange.Protocol as BT
17import Data.Torrent.Block as BT
18import Data.Torrent.Bitfield as BT
19
20instance KRPC ByteString ByteString where
21 method = "echo"
22
23instance MonadLogger IO where
24 monadLoggerLog _ _ _ _ = return ()
25
26
27instance NFData PortNumber where
28 rnf = rnf . (fromIntegral :: PortNumber -> Int)
29
30instance NFData BlockIx where
31 rnf (BlockIx a b c) = a `deepseq` b `deepseq` rnf c
32
33instance NFData Block where
34 rnf (Block a b c) = a `deepseq` b `deepseq` rnf c
35
36instance NFData Bitfield
37
38instance NFData Message where
39 rnf (Have i) = rnf i
40 rnf (Bitfield b) = rnf b
41 rnf (Request b) = rnf b
42 rnf (Piece b) = rnf b
43 rnf (Cancel b) = rnf b
44 rnf (Port i) = rnf i
45 rnf _ = () -- other fields are forced by pattern matching
46
47{-
48encodeMessages :: [Message] -> ByteString
49encodeMessages xs = runPut (mapM_ put xs)
50
51decodeMessages :: ByteString -> Either String [Message]
52decodeMessages = runGet (many get)
53-}
54
55echo :: Handler IO
56echo = handler $ \ _ bs -> return (bs :: ByteString)
57
58addr :: SockAddr
59addr = SockAddrInet 6000 (256 * 256 * 256 + 127)
60
61-- main :: IO ()
62-- main = defaultMain []
63main :: IO ()
64main = withManager def addr [echo] $ \ m -> (`runReaderT` m) $ do
65 listen
66 liftIO $ defaultMain (benchmarks m)
67 where
68 sizes = [10, 100, 1000, 10000, 16 * 1024]
69 repetitions = [1, 10, 100, 1000]
70 benchmarks m = [mkbench m r s | r <- repetitions, s <- sizes]
71 where
72 mkbench action r n =
73 bench (show r ++ "times" ++ "/" ++ show n ++ "bytes") $ nfIO $
74 replicateM r $
75 runReaderT (query addr (BS.replicate n 0)) action
diff --git a/bittorrent/bench/Throughtput.hs b/bittorrent/bench/Throughtput.hs
new file mode 100644
index 00000000..d0404405
--- /dev/null
+++ b/bittorrent/bench/Throughtput.hs
@@ -0,0 +1,46 @@
1{-# LANGUAGE ViewPatterns #-}
2{-# LANGUAGE PatternGuards #-}
3module Main (main) where
4
5import Control.Concurrent
6import Data.Bitfield
7import Network.BitTorrent
8import System.Environment
9import Control.Monad.Reader
10import Data.IORef
11
12
13main :: IO ()
14main = do
15 [path] <- getArgs
16 torrent <- fromFile path
17
18 print (contentLayout "./" (tInfo torrent))
19
20 client <- newClient 100 []
21 swarm <- newLeecher client torrent
22
23 ref <- liftIO $ newIORef 0
24 discover swarm $ do
25 forever $ do
26 e <- awaitEvent
27 case e of
28 Available bf
29 | Just m <- findMin bf -> yieldEvent (Want (BlockIx m 0 10))
30 | otherwise -> return ()
31 Want bix -> liftIO $ print bix
32 Fragment blk -> do
33
34 sc <- liftIO $ getSessionCount swarm
35 addr <- asks connectedPeerAddr
36
37 liftIO $ do
38 x <- atomicModifyIORef ref (\x -> (succ x, x))
39 if x `mod` 100 == 0
40 then print (x, sc, addr)
41 else return ()
42
43 yieldEvent (Want (BlockIx 0 0 (16 * 1024)))
44
45
46 print "Bye-bye! =_=" \ No newline at end of file
diff --git a/bittorrent/bench/TorrentFile.hs b/bittorrent/bench/TorrentFile.hs
new file mode 100644
index 00000000..e91a9c10
--- /dev/null
+++ b/bittorrent/bench/TorrentFile.hs
@@ -0,0 +1,27 @@
1{-# LANGUAGE BangPatterns #-}
2module Main (main) where
3
4import Data.BEncode
5import Data.ByteString as BS
6import Data.Torrent
7import Criterion.Main
8
9
10tinyPath :: FilePath
11tinyPath = "res/dapper-dvd-amd64.iso.torrent"
12
13largePath :: FilePath
14largePath = "res/pkg.torrent"
15
16decoder :: ByteString -> Torrent
17decoder bs = let Right r = decode bs in r
18
19main :: IO ()
20main = do
21 !tinyBin <- BS.readFile tinyPath
22 !largeBin <- BS.readFile largePath
23
24 defaultMain
25 [ bench "read/tiny" $ nf decoder tinyBin
26 , bench "read/large" $ nf decoder largeBin
27 ] \ No newline at end of file
diff --git a/bittorrent/bittorrent.cabal b/bittorrent/bittorrent.cabal
new file mode 100644
index 00000000..8ec314e7
--- /dev/null
+++ b/bittorrent/bittorrent.cabal
@@ -0,0 +1,412 @@
1name: bittorrent
2version: 0.0.0.3
3license: BSD3
4license-file: LICENSE
5author: Sam Truzjan
6maintainer: Sam Truzjan <pxqr.sta@gmail.com>
7copyright: (c) 2013, Sam Truzjan
8category: Network
9build-type: Custom
10cabal-version: >= 1.10
11tested-with: GHC == 7.6.3
12homepage: https://github.com/cobit/bittorrent
13bug-reports: https://github.com/cobit/bittorrent/issues
14synopsis: BitTorrent protocol implementation.
15description:
16
17 A library for making Haskell bittorrent applications easy.
18 .
19 For more information see:
20 <https://github.com/cobit/bittorrent/blob/master/README.md>
21
22extra-source-files: res/dapper-dvd-amd64.iso.torrent
23 res/pkg.torrent
24 README.md
25 ChangeLog
26 cbits/*.h
27
28
29source-repository head
30 type: git
31 location: git://github.com/cobit/bittorrent.git
32
33source-repository this
34 type: git
35 location: git://github.com/cobit/bittorrent.git
36 branch: master
37 tag: v0.0.0.3
38
39flag testing
40 description: Whether to build tests.
41 default: False
42
43flag examples
44 description: Whether to build examples.
45 default: False
46
47flag network-uri
48 description: Use network-uri package.
49 default: True
50
51flag bits-extras
52 description: Use more-effecient bits-extras bitwise operations.
53 default: False
54
55flag dht-only
56 description: Build only DHT related modules.
57 default: True
58
59flag builder
60 description: Use older bytestring package and bytestring-builder.
61 default: False
62
63flag aeson
64 description: Use aeson for pretty-printing bencoded data.
65 default: True
66
67flag thread-debug
68 description: Add instrumentation to threads.
69 default: True
70
71library
72 default-language: Haskell2010
73 default-extensions: PatternGuards
74 , OverloadedStrings
75 , RecordWildCards
76 hs-source-dirs: src, cryptonite-backport, .
77 exposed-modules: Network.SocketLike
78 Data.Digest.CRC32C
79 Data.Bits.ByteString
80 Data.Wrapper.PSQ
81 Data.Wrapper.PSQInt
82 Data.MinMaxPSQ
83 Network.Address
84 Network.Kademlia.Routing
85 Data.Torrent
86 Network.BitTorrent.DHT.ContactInfo
87 Network.BitTorrent.DHT.Token
88 Network.Kademlia.Search
89 Network.QueryResponse
90 Network.StreamServer
91 Data.BEncode.Pretty
92 Control.Concurrent.Tasks
93 Network.Kademlia
94 Network.BitTorrent.MainlineDHT
95 System.Global6
96 Network.Tox
97 Network.Tox.Transport
98 Network.Tox.Crypto.Transport
99 Network.Tox.Onion.Handlers
100 Network.Tox.Onion.Transport
101 Network.Tox.DHT.Handlers
102 Network.Tox.DHT.Transport
103 Network.Tox.NodeId
104 Control.TriadCommittee
105 Crypto.Tox
106 Text.XXD
107
108 build-depends: base
109 , containers
110 , array
111 , hashable
112 , iproute
113 , stm
114 , base16-bytestring
115 , base32-bytestring
116 , base64-bytestring
117 , psqueues
118 , reflection
119 , deepseq
120 , text
121 , filepath
122 , directory
123 , bencoding
124 , contravariant
125
126 , cryptonite
127 , memory
128 , time
129 , random
130 , entropy
131 , cpu
132
133 , cereal
134 , http-types
135
136 , process
137 , split
138 , pretty
139 , convertible
140 , data-default
141
142 , bifunctors
143 , lens
144 , lifted-async
145 , lifted-base
146 , monad-control
147 , transformers-base
148 , mtl
149
150 if flag(network-uri)
151 Build-depends: network >= 2.6
152 , network-uri >= 2.6
153 else
154 Build-depends: network >= 2.4 && < 2.6
155
156
157 other-modules: Paths_bittorrent
158 Crypto.Cipher.Salsa
159 Crypto.Cipher.XSalsa
160 Crypto.ECC.Class
161 Crypto.ECC.Simple.Prim
162 Crypto.ECC.Simple.Types
163 Crypto.Error.Types
164 Crypto.Internal.ByteArray
165 Crypto.Internal.Compat
166 Crypto.Internal.DeepSeq
167 Crypto.Internal.Imports
168 Crypto.PubKey.Curve25519
169
170 C-sources: cbits/cryptonite_xsalsa.c, cbits/cryptonite_salsa.c
171
172 if !flag(dht-only)
173 exposed-modules: Network.BitTorrent
174 Network.BitTorrent.Client
175 Network.BitTorrent.Client.Types
176 Network.BitTorrent.Client.Handle
177 Network.BitTorrent.Exchange
178 Network.BitTorrent.Exchange.Bitfield
179 Network.BitTorrent.Exchange.Block
180 Network.BitTorrent.Exchange.Connection
181 Network.BitTorrent.Exchange.Download
182 Network.BitTorrent.Exchange.Manager
183 Network.BitTorrent.Exchange.Message
184 Network.BitTorrent.Exchange.Session
185 Network.BitTorrent.Tracker
186 Network.BitTorrent.Tracker.List
187 Network.BitTorrent.Tracker.Message
188 Network.BitTorrent.Tracker.RPC
189 Network.BitTorrent.Tracker.RPC.HTTP
190 Network.BitTorrent.Tracker.RPC.UDP
191 Network.BitTorrent.Tracker.Session
192 System.Torrent.Storage
193 if !flag(dht-only)
194 if flag(testing)
195 exposed-modules:
196 Network.BitTorrent.Internal.Cache
197 Network.BitTorrent.Internal.Progress
198 Network.BitTorrent.Internal.Types
199 System.Torrent.FileMap
200 System.Torrent.Tree
201 else
202 other-modules:
203 Network.BitTorrent.Internal.Cache
204 Network.BitTorrent.Internal.Progress
205 Network.BitTorrent.Internal.Types
206 System.Torrent.FileMap
207 System.Torrent.Tree
208 if flag(aeson)
209 build-depends: aeson, aeson-pretty, unordered-containers, vector
210 cpp-options: -DBENCODE_AESON
211 if flag(thread-debug)
212 exposed-modules: Control.Concurrent.Lifted.Instrument
213 Control.Concurrent.Async.Lifted.Instrument
214 cpp-options: -DTHREAD_DEBUG
215
216 if flag(builder)
217 build-depends: bytestring >= 0.9, bytestring-builder
218 else
219 build-depends: bytestring >= 0.10
220 if impl(ghc < 7.6)
221 build-depends: ghc-prim
222 ghc-options: -Wall -fdefer-typed-holes
223 ghc-prof-options:
224
225
226test-suite spec
227 if !flag(testing)
228 buildable: False
229 default-language: Haskell2010
230 default-extensions: OverloadedStrings
231 type: exitcode-stdio-1.0
232 hs-source-dirs: tests
233 main-is: Main.hs
234 other-modules: Spec
235 Config
236 Network.KRPCSpec
237 Network.KRPC.MethodSpec
238 Network.DatagramServer.MainlineSpec
239 Data.TorrentSpec
240 Network.BitTorrent.Client.HandleSpec
241 Network.BitTorrent.CoreSpec
242 Network.BitTorrent.DHTSpec
243 Network.BitTorrent.DHT.TestData
244 Network.BitTorrent.DHT.MessageSpec
245 Network.BitTorrent.DHT.QuerySpec
246 Network.Kademlia.RoutingSpec
247 Network.BitTorrent.DHT.SessionSpec
248 Network.BitTorrent.DHT.TokenSpec
249 Network.BitTorrent.Internal.CacheSpec
250 Network.BitTorrent.Internal.ProgressSpec
251 Network.BitTorrent.Tracker.TestData
252 Network.BitTorrent.Tracker.ListSpec
253 Network.BitTorrent.Tracker.MessageSpec
254 Network.BitTorrent.Tracker.RPCSpec
255 Network.BitTorrent.Tracker.RPC.HTTPSpec
256 Network.BitTorrent.Tracker.RPC.UDPSpec
257 Network.BitTorrent.Tracker.SessionSpec
258 Network.BitTorrent.Exchange.BitfieldSpec
259 Network.BitTorrent.Exchange.ConnectionSpec
260 Network.BitTorrent.Exchange.DownloadSpec
261 Network.BitTorrent.Exchange.MessageSpec
262 Network.BitTorrent.Exchange.SessionSpec
263 System.Torrent.StorageSpec
264 System.Torrent.FileMapSpec
265 build-depends: base == 4.*
266
267 -- * Concurrency
268 , async
269
270 -- * Data
271 , bytestring
272 , bytestring-arbitrary
273 , containers
274 , convertible
275 , data-default
276 , text
277 , time
278
279 -- * Serialization
280 , cereal
281
282 -- * Monads
283 , mtl
284 , resourcet
285 , conduit
286 , conduit-extra
287 , monad-loops
288 , monad-logger
289
290 -- * Network
291 , http-types
292 , iproute
293
294 -- * System
295 , optparse-applicative >= 0.8
296 , process
297 , directory
298 , filepath
299
300 -- * Testing
301 , hspec >= 1.8.2
302 , QuickCheck
303 , quickcheck-instances
304
305 -- * Bittorrent
306 , bittorrent
307 , temporary
308 , bencoding >= 0.4.3
309 if flag(network-uri)
310 Build-depends: network >= 2.6
311 , network-uri >= 2.6
312 else
313 Build-depends: network >= 2.4 && < 2.6
314 ghc-options: -Wall -fno-warn-orphans
315
316
317--benchmark bench
318-- default-language: Haskell2010
319-- default-extensions:
320-- type: exitcode-stdio-1.0
321-- hs-source-dirs: bench
322-- main-is: Main.hs
323-- build-depends: base
324-- , bytestring
325-- , cereal
326-- , network
327--
328-- , criterion
329-- , deepseq
330--
331-- , bittorrent
332-- ghc-options: -O2 -Wall -fno-warn-orphans
333benchmark bench
334 type: exitcode-stdio-1.0
335 default-language: Haskell2010
336 hs-source-dirs: bench
337 main-is: Main.hs
338 build-depends: base == 4.*
339 , bytestring
340 , mtl
341 , monad-logger
342 , criterion
343 ghc-options: -O2 -fforce-recomp
344
345executable dht
346 hs-source-dirs: examples
347 main-is: dht.hs
348 default-language: Haskell2010
349 build-depends: base, haskeline, network, bytestring, transformers
350
351executable dhtd
352 hs-source-dirs: examples
353 main-is: dhtd.hs
354 default-language: Haskell2010
355 build-depends: base, network, bytestring, hashable, deepseq
356 , aeson
357 , pretty
358 , bittorrent
359 , unix
360 , containers
361 , stm
362 , cereal
363 , bencoding
364 if flag(thread-debug)
365 build-depends: time
366 cpp-options: -DTHREAD_DEBUG
367
368-- Utility to work with torrent files.
369executable mktorrent
370 if !flag(examples)
371 buildable: False
372 default-language: Haskell2010
373 hs-source-dirs: examples
374 main-is: MkTorrent.hs
375 other-modules: Paths_bittorrent
376 build-depends: base == 4.*
377 , bytestring
378 , text
379 , pretty
380
381 , mtl
382 , conduit
383 , lens
384 , lifted-async
385 , parallel-io
386
387 , bittorrent
388
389 , filepath
390 , optparse-applicative
391 , hslogger
392-- if flag(network-uri)
393-- Build-depends:
394 , network >= 2.6
395 , network-uri >= 2.6
396-- else
397-- Build-depends: network >= 2.4 && < 2.6
398 ghc-options: -Wall -O2 -threaded
399
400-- nonfunctioning example of very basic bittorrent client
401executable client
402 if !flag(examples)
403 buildable: False
404 default-language: Haskell2010
405 hs-source-dirs: examples
406 main-is: Client.hs
407 build-depends: base == 4.*
408 , bittorrent
409 , mtl
410 , pretty
411 , data-default
412 , optparse-applicative
diff --git a/bittorrent/dev/README.md b/bittorrent/dev/README.md
new file mode 100644
index 00000000..e2cc51a6
--- /dev/null
+++ b/bittorrent/dev/README.md
@@ -0,0 +1,4 @@
1This directory is for some dev scripts and other dev only stuff which
2we don't want to keep in the resulting `cabal sdist` generated
3tarball. Do _not_ include any of these files to .cabal file, neither
4to `extra-source-files` nor to `data-files` sections.
diff --git a/bittorrent/dev/add-sources.sh b/bittorrent/dev/add-sources.sh
new file mode 100755
index 00000000..e125cade
--- /dev/null
+++ b/bittorrent/dev/add-sources.sh
@@ -0,0 +1,5 @@
1#!/bin/bash
2
3for s in $(ls $(dirname $0)/../sub); do
4 (cd $(dirname $0)/.. && cabal sandbox add-source sub/$s)
5done
diff --git a/bittorrent/dev/bench b/bittorrent/dev/bench
new file mode 100755
index 00000000..5d03db3f
--- /dev/null
+++ b/bittorrent/dev/bench
@@ -0,0 +1,4 @@
1#!/bin/sh
2cabal-dev build &&
3 ./dist/build/benchmarks/benchmarks -o dist/build/benchmarks/result.html &&
4 xdg-open dist/build/benchmarks/result.html \ No newline at end of file
diff --git a/bittorrent/dev/test b/bittorrent/dev/test
new file mode 100755
index 00000000..2eb85df2
--- /dev/null
+++ b/bittorrent/dev/test
@@ -0,0 +1,2 @@
1#!/bin/sh
2cabal-dev build && cabal-dev test || echo "ERROR: Some tests failed."
diff --git a/bittorrent/dev/update-dependencies.sh b/bittorrent/dev/update-dependencies.sh
new file mode 100755
index 00000000..c83694c3
--- /dev/null
+++ b/bittorrent/dev/update-dependencies.sh
@@ -0,0 +1,11 @@
1#!/bin/sh
2
3cd $(dirname $0)/..
4
5git submodule init
6git submodule foreach git fetch
7git submodule update --recursive --checkout --force
8
9$(dirname $0)/add-sources.sh
10
11cabal install --enable-tests --only-dependencies --reinstall
diff --git a/bittorrent/examples/Client.hs b/bittorrent/examples/Client.hs
new file mode 100644
index 00000000..26711676
--- /dev/null
+++ b/bittorrent/examples/Client.hs
@@ -0,0 +1,74 @@
1{-# LANGUAGE RankNTypes #-}
2{-# LANGUAGE CPP #-}
3{-# LANGUAGE ExistentialQuantification #-}
4{-# LANGUAGE RecordWildCards #-}
5module Main (main) where
6import Control.Concurrent
7import Control.Monad.Trans
8import Data.Maybe
9import Options.Applicative
10import System.Environment
11import System.Exit
12import System.IO
13import Text.Read
14
15import Network.BitTorrent
16
17#if MIN_VERSION_optparse_applicative(0,13,0)
18-- maybeReader imported from Options.Applicative.Builder
19#elif MIN_VERSION_optparse_applicative(0,11,0)
20maybeReader f = eitherReader (maybe (Left ":(") Right . f)
21#else
22maybeReader f = f
23#endif
24
25{-----------------------------------------------------------------------
26-- Command line arguments
27-----------------------------------------------------------------------}
28
29data TorrentBox = forall s. TorrentSource s => TorrentBox { unTorrentBox :: s }
30
31data Args = Args
32 { topic :: TorrentBox
33 , contentDir :: FilePath
34 }
35
36argsParser :: Parser Args
37argsParser = Args <$> (TorrentBox <$> infohashP <|> TorrentBox <$> torrentP)
38 <*> destDirP
39 where
40 infohashP :: Parser InfoHash
41 infohashP = argument (maybeReader readMaybe)
42 (metavar "SHA1" <> help "infohash of torrent file")
43
44 torrentP :: Parser FilePath
45 torrentP = argument (maybeReader Just)
46 ( metavar "FILE"
47 <> help "A .torrent file"
48 )
49
50 destDirP :: Parser FilePath
51 destDirP = argument (maybeReader Just)
52 ( metavar "DIR"
53 <> help "Directory to put content"
54 )
55
56argsInfo :: ParserInfo Args
57argsInfo = info (helper <*> argsParser)
58 ( fullDesc
59 <> progDesc "A simple CLI bittorrent client"
60 <> header "foo"
61 )
62
63{-----------------------------------------------------------------------
64-- Client
65-----------------------------------------------------------------------}
66
67run :: Args -> BitTorrent ()
68run (Args (TorrentBox t) dir) = do
69 h <- openHandle dir t
70 start h
71 liftIO $ threadDelay 10000000000
72
73main :: IO ()
74main = execParser argsInfo >>= simpleClient . run
diff --git a/bittorrent/examples/FS.hs b/bittorrent/examples/FS.hs
new file mode 100644
index 00000000..550d85a7
--- /dev/null
+++ b/bittorrent/examples/FS.hs
@@ -0,0 +1,74 @@
1{-# LANGUAGE RecordWildCards #-}
2module Main (main) where
3
4import Control.Arrow
5import Data.ByteString.Char8 as BC
6import Data.List as L
7import Data.Map as M
8import Data.Torrent as T
9import Data.Torrent.Tree as T
10import System.Environment
11import System.Fuse
12import System.FilePath
13import System.Posix.Files
14
15
16defStat :: FileStat
17defStat = FileStat
18 { statEntryType = Unknown
19 , statFileMode = ownerReadMode
20 , statLinkCount = 2
21
22 , statFileOwner = 0
23 , statFileGroup = 0
24
25 , statSpecialDeviceID = 0
26
27 , statFileSize = 0
28 , statBlocks = 0
29
30 , statAccessTime = 0
31 , statModificationTime = 0
32 , statStatusChangeTime = 0
33 }
34
35dirStat :: FileStat
36dirStat = defStat {
37 statEntryType = Directory
38 }
39
40type Result a = IO (Either Errno a)
41type Result' = IO Errno
42
43fsGetFileStat :: Torrent -> FilePath -> Result FileStat
44fsGetFileStat _ path = return $ Right dirStat
45
46fsOpenDirectory :: Torrent -> FilePath -> Result'
47fsOpenDirectory _ _ = return eOK
48
49fsReadDirectory :: Torrent -> FilePath -> Result [(FilePath, FileStat)]
50fsReadDirectory Torrent {tInfoDict = InfoDict {..}} path
51 | Just cs <- T.lookupDir (L.tail (splitDirectories path)) tree =
52 return $ Right $ L.map (BC.unpack *** const defStat) cs
53 | otherwise = return $ Left eNOENT
54 where
55 tree = build $ idLayoutInfo
56
57fsReleaseDirectory :: Torrent -> FilePath -> Result'
58fsReleaseDirectory _ _ = return eOK
59
60exfsOps :: Torrent -> FuseOperations ()
61exfsOps t = defaultFuseOps
62 { fuseGetFileStat = fsGetFileStat t
63
64 , fuseOpenDirectory = fsOpenDirectory t
65 , fuseReadDirectory = fsReadDirectory t
66 , fuseReleaseDirectory = fsReleaseDirectory t
67 }
68
69main :: IO ()
70main = do
71 x : xs <- getArgs
72 t <- fromFile x
73 withArgs xs $ do
74 fuseMain (exfsOps t) defaultExceptionHandler \ No newline at end of file
diff --git a/bittorrent/examples/MkTorrent.hs b/bittorrent/examples/MkTorrent.hs
new file mode 100644
index 00000000..88a84893
--- /dev/null
+++ b/bittorrent/examples/MkTorrent.hs
@@ -0,0 +1,500 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE RecordWildCards #-}
4{-# LANGUAGE StandaloneDeriving #-}
5{-# OPTIONS -fno-warn-orphans #-}
6module Main (main) where
7
8import Prelude as P
9import Control.Concurrent
10import Control.Concurrent.Async.Lifted
11import Control.Concurrent.ParallelIO
12import Control.Exception
13import Control.Lens hiding (argument, (<.>))
14import Control.Monad as M
15import Control.Monad.Trans
16import Data.Conduit as C
17import Data.Conduit.List as C
18import Data.List as L
19import Data.Maybe as L
20import Data.Monoid
21import Data.Text as T
22import qualified Data.Text.IO as T
23import Data.Text.Read as T
24import Data.Version
25import Network
26import Network.URI
27import Options.Applicative
28import System.Exit
29import System.FilePath
30import System.Log
31import System.Log.Logger
32import Text.Read
33import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
34
35import Paths_bittorrent (version)
36import Data.Torrent hiding (Magnet (Magnet))
37import Network.Address
38import Network.BitTorrent.DHT.Session hiding (Options, options)
39import Network.BitTorrent.DHT as DHT hiding (Options)
40import Network.BitTorrent.Exchange.Bitfield as BF
41import Network.BitTorrent.Exchange.Connection hiding (Options)
42import Network.BitTorrent.Exchange.Message
43import Network.BitTorrent.Exchange.Session
44import System.Torrent.Storage
45
46#if MIN_VERSION_optparse_applicative(0,13,0)
47-- maybeReader imported from Options.Applicative.Builder
48#elif MIN_VERSION_optparse_applicative(0,11,0)
49maybeReader f = eitherReader (maybe (Left ":(") Right . f)
50#else
51maybeReader f = f
52#endif
53
54
55{-----------------------------------------------------------------------
56-- Dialogs
57-----------------------------------------------------------------------}
58
59instance Read URI where
60 readsPrec _ = f . parseURI
61 where
62 f Nothing = []
63 f (Just u) = [(u, "")]
64
65question :: Show a => Text -> Maybe a -> IO ()
66question q defVal = do
67 T.putStrLn q
68 case defVal of
69 Nothing -> return ()
70 Just v -> T.putStrLn $ "[default: " <> T.pack (show v) <> "]"
71
72ask :: Read a => Text -> IO a
73ask q = question q (Just True) >> getReply
74 where
75 getReply = do
76 resp <- P.getLine
77 maybe getReply return $ readMaybe resp
78
79askMaybe :: Read a => Text -> IO (Maybe a)
80askMaybe q = question q (Just False) >> getReply
81 where
82 getReply = do
83 resp <- P.getLine
84 if resp == []
85 then return Nothing
86 else maybe getReply return $ readMaybe resp
87
88askURI :: IO URI
89askURI = do
90 s <- P.getLine
91 case parseURI s of
92 Nothing -> T.putStrLn "incorrect URI" >> askURI
93 Just u -> return u
94
95askFreeform :: IO Text
96askFreeform = do
97 s <- T.getLine
98 if T.null s
99 then askFreeform
100 else return s
101
102askInRange :: Int -> Int -> IO Int
103askInRange a b = do
104 s <- T.getLine
105 case T.decimal s of
106 Left msg -> do
107 P.putStrLn msg
108 askInRange a b
109 Right (i, _)
110 | a <= i && i < b -> return i
111 | otherwise -> do
112 T.putStrLn "not in range "
113 askInRange a b
114
115askChoice :: [(Text, a)] -> IO a
116askChoice kvs = do
117 forM_ (L.zip [1 :: Int ..] $ L.map fst kvs) $ \(i, lbl) -> do
118 T.putStrLn $ " " <> T.pack (show i) <> ") " <> lbl
119 T.putStrLn "Your choice?"
120 n <- askInRange 1 (succ (L.length kvs))
121 return $ snd (kvs !! pred n)
122
123{-----------------------------------------------------------------------
124-- Helpers
125-----------------------------------------------------------------------}
126
127torrentFile :: Parser FilePath
128torrentFile = argument (maybeReader Just)
129 ( metavar "TORRENT_FILE_PATH"
130 <> help "A .torrent file"
131 )
132
133{-----------------------------------------------------------------------
134-- Amend command - edit a field of torrent file
135-----------------------------------------------------------------------}
136
137data AmendOpts = AmendOpts FilePath
138 deriving Show
139
140amendInfo :: ParserInfo AmendOpts
141amendInfo = info (helper <*> parser) modifier
142 where
143 modifier = progDesc "Edit info fields of existing torrent"
144 parser = AmendOpts <$> torrentFile
145
146type Amend = Torrent -> Torrent
147
148fields :: [(Text, IO Amend)]
149fields = [ ("announce", set announce . Just <$> askURI)
150 , ("comment", set comment . Just <$> askFreeform)
151 , ("created by", set createdBy . Just <$> askFreeform)
152 , ("publisher url", set publisherURL . Just <$> askURI)
153 ]
154
155askAmend :: IO Amend
156askAmend = join $ T.putStrLn "Choose a field:" >> askChoice fields
157
158amend :: AmendOpts -> IO ()
159amend (AmendOpts tpath) = do
160 t <- fromFile tpath
161 a <- askAmend
162 toFile tpath $ a t
163
164{-----------------------------------------------------------------------
165-- Check command -- validate content files using torrent file
166-----------------------------------------------------------------------}
167-- TODO progress bar
168
169data CheckOpts = CheckOpts
170 { checkTorrentPath :: FilePath -- ^ validation torrent file
171 , checkContentPath :: FilePath -- ^ root dir for content files
172 } deriving Show
173
174checkInfo :: ParserInfo CheckOpts
175checkInfo = info (helper <*> parser) modifier
176 where
177 modifier = progDesc "Validate integrity of torrent data"
178 <> header "append +RTS -N$NUMBER_OF_CORES -RTS for parallel execution"
179 parser = CheckOpts
180 <$> torrentFile
181 <*> argument (maybeReader Just)
182 ( metavar "CONTENT_DIR_PATH"
183 <> value "."
184 <> help "Content directory or a single file"
185 )
186
187validatePiece :: Storage -> PieceInfo -> PieceIx -> IO (Maybe PieceIx)
188validatePiece s pinfo pix = do
189 valid <- verifyPiece s pinfo pix
190 if valid
191 then do infoM "check" $ "valid piece " ++ show pix
192 return (Just pix)
193 else do infoM "check" $ "invalid piece " ++ show pix
194 return Nothing
195
196validateStorage :: Storage -> PieceInfo -> IO Bitfield
197validateStorage s pinfo = do
198 infoM "check" "start storage validation"
199 let total = totalPieces s
200 pixs <- parallel $ L.map (validatePiece s pinfo) [0 .. total - 1]
201 infoM "check" "storage validation finished"
202 return $ fromList total $ L.catMaybes pixs
203
204-- TODO use local thread pool
205checkContent :: Storage -> PieceInfo -> IO ()
206checkContent s pinfo = do
207 invalids <- BF.complement <$> validateStorage s pinfo
208 if BF.null invalids
209 then noticeM "check" "all files are complete and valid"
210 else do
211 emergencyM "check" $ "there are some invalid pieces" ++ show invalids
212 exitFailure
213
214checkTorrent :: CheckOpts -> IO ()
215checkTorrent CheckOpts {..} = do
216 infoM "check" "openning torrent file..."
217 InfoDict {..} <- tInfoDict <$> fromFile checkTorrentPath
218 let layout = flatLayout checkContentPath idLayoutInfo
219 infoM "check" "mapping content files..."
220 withStorage ReadOnly (piPieceLength idPieceInfo) layout $ \ s -> do
221 infoM "check" "files mapped"
222 checkContent s idPieceInfo
223 infoM "check" "unmapping files"
224
225{-----------------------------------------------------------------------
226-- Create command
227-----------------------------------------------------------------------}
228-- TODO progress bar
229-- TODO multifile torrents
230-- TODO interactive mode
231-- TODO non interactive mode
232-- TODO --ignore-dot-files
233-- TODO --md5
234-- TODO --piece-size
235
236{-
237createFlags :: Parser CreateFlags
238createFlags = CreateFlags
239 <$> optional (option
240 ( long "piece-size"
241 <> short 's'
242 <> metavar "SIZE"
243 <> help "Set size of torrent pieces"
244 ))
245 <*> switch
246 ( long "md5"
247 <> short '5'
248 <> help "Include md5 hash of each file"
249 )
250 <*> switch
251 ( long "ignore-dot-files"
252 <> short 'd'
253 <> help "Do not include .* files"
254 )
255
256
257createOpts :: Parser CreateOpts
258createOpts = CreateOpts
259 <$> argument (maybeReader Just)
260 ( metavar "PATH"
261 <> help "Content directory or a single file"
262 )
263 <*> optional (argument (maybeReader Just)
264 ( metavar "FILE"
265 <> help "Place for the output .torrent file"
266 ))
267 <*> createFlags
268
269createInfo :: ParserInfo CreateOpts
270createInfo = info (helper <*> createOpts) modifier
271 where
272 modifier = progDesc "Make a new .torrent file"
273-}
274
275{-----------------------------------------------------------------------
276-- Magnet command -- print magnet link for given torrent file
277-----------------------------------------------------------------------}
278
279data MagnetOpts = MagnetOpts
280 { magnetFile :: FilePath -- ^ path to torrent file
281 , detailed :: Bool -- ^ whether to append additional uri params
282 } deriving Show
283
284magnetInfo :: ParserInfo MagnetOpts
285magnetInfo = info (helper <*> parser) modifier
286 where
287 modifier = progDesc "Print magnet link"
288 parser = MagnetOpts
289 <$> torrentFile
290 <*> switch ( long "detailed" )
291
292magnet :: MagnetOpts -> IO ()
293magnet MagnetOpts {..} = print . magnetLink =<< fromFile magnetFile
294 where
295 magnetLink = if detailed then detailedMagnet else simpleMagnet
296
297{-----------------------------------------------------------------------
298-- Show command - print torrent file information
299-----------------------------------------------------------------------}
300
301data ShowOpts = ShowOpts
302 { showPath :: FilePath -- ^ torrent file to inspect;
303 , infoHashOnly :: Bool -- ^ omit everything except infohash.
304 } deriving Show
305
306showInfo :: ParserInfo ShowOpts
307showInfo = info (helper <*> parser) modifier
308 where
309 modifier = progDesc "Print .torrent file metadata"
310 parser = ShowOpts
311 <$> torrentFile
312 <*> switch
313 ( long "infohash"
314 <> help "Show only hash of the torrent info part"
315 )
316
317showTorrent :: ShowOpts -> Torrent -> ShowS
318showTorrent ShowOpts {..} torrent
319 | infoHashOnly = shows $ idInfoHash (tInfoDict torrent)
320 | otherwise = shows $ pPrint torrent
321
322putTorrent :: ShowOpts -> IO ()
323putTorrent opts @ ShowOpts {..} = do
324 torrent <- fromFile showPath `onException` putStrLn msg
325 putStrLn $ showTorrent opts torrent []
326 where
327 msg = "Torrent file is either invalid or do not exist"
328
329{-----------------------------------------------------------------------
330-- Get command - fetch torrent by infohash
331-----------------------------------------------------------------------}
332
333data GetOpts = GetOpts
334 { topic :: InfoHash
335 , servPort :: PortNumber
336 , bootNode :: NodeAddr IPv4
337 , buckets :: Int
338 } deriving Show
339
340#if !MIN_VERSION_network(2,6,3)
341instance Read PortNumber where
342 readsPrec i s = [ (toEnum a, t) | (a, t) <- readsPrec i s]
343#endif
344
345paramsParser :: Parser GetOpts
346paramsParser = GetOpts
347 <$> argument (maybeReader readMaybe)
348 (metavar "SHA1" <> help "infohash of torrent file")
349 <*> option auto (long "port" <> short 'p'
350 <> value 7000 <> showDefault
351 <> metavar "NUM" <> help "port number to bind"
352 )
353 <*> option auto (long "boot" <> short 'b'
354 <> metavar "NODE" <> help "bootstrap node address"
355 )
356 <*> option auto (long "bucket" <> short 'n'
357 <> value 2 <> showDefault
358 <> metavar "NUM" <> help "number of buckets to maintain"
359 )
360
361getInfo :: ParserInfo GetOpts
362getInfo = info (helper <*> paramsParser)
363 ( fullDesc
364 <> progDesc "Get torrent file by infohash"
365 <> header "get torrent file by infohash"
366 )
367
368 -- TODO add tNodes, tCreated, etc?
369getTorrent :: GetOpts -> IO ()
370getTorrent GetOpts {..} = do
371 infoM "get" "searching for peers..."
372 s <- newSession (\ _ _ _ _ -> return ()) (PeerAddr Nothing Nothing 7000) "/tmp" (Left topic)
373 dht (def { optBucketCount = buckets }) (NodeAddr "0.0.0.0" servPort) $ do
374 bootstrap [bootNode]
375 infodict <- withAsync (DHT.lookup topic $$ connectSink s)
376 (const (liftIO $ waitMetadata s))
377 liftIO $ toFile (show topic <.> torrentExt) $ nullTorrent infodict
378 infoM "get" "saved torrent file"
379
380{-----------------------------------------------------------------------
381-- Command
382-----------------------------------------------------------------------}
383
384data Command
385 = Amend AmendOpts
386 | Check CheckOpts
387-- | Create CreateOpts
388 | Get GetOpts
389 | Magnet MagnetOpts
390 | Show ShowOpts
391 deriving Show
392
393commandOpts :: Parser Command
394commandOpts = subparser $ mconcat
395 [ command "amend" (Amend <$> amendInfo)
396 , command "check" (Check <$> checkInfo)
397-- , command "create" (Create <$> createInfo)
398 , command "get" (Get <$> getInfo)
399 , command "magnet" (Magnet <$> magnetInfo)
400 , command "show" (Show <$> showInfo)
401 ]
402
403{-----------------------------------------------------------------------
404-- Global Options
405-----------------------------------------------------------------------}
406
407data GlobalOpts = GlobalOpts
408 { verbosity :: Priority
409 } deriving Show
410
411#if !MIN_VERSION_hslogger(1,2,9)
412deriving instance Enum Priority
413deriving instance Bounded Priority
414#endif
415
416priorities :: [Priority]
417priorities = [minBound..maxBound]
418
419defaultPriority :: Priority
420defaultPriority = WARNING
421
422verbosityOpts :: Parser Priority
423verbosityOpts = verbosityP <|> verboseP <|> quietP
424 where
425 verbosityP = option auto
426 ( long "verbosity"
427 <> metavar "LEVEL"
428 <> help ("Set verbosity level\n"
429 ++ "Possible values are " ++ show priorities)
430 )
431
432 verboseP = flag defaultPriority INFO
433 ( long "verbose"
434 <> short 'v'
435 <> help "Verbose mode"
436 )
437
438 quietP = flag defaultPriority CRITICAL
439 ( long "quiet"
440 <> short 'q'
441 <> help "Silent mode"
442 )
443
444
445globalOpts :: Parser GlobalOpts
446globalOpts = GlobalOpts <$> verbosityOpts
447
448data Options = Options
449 { cmdOpts :: Command
450 , globOpts :: GlobalOpts
451 } deriving Show
452
453options :: Parser Options
454options = Options <$> commandOpts <*> globalOpts
455
456versioner :: String -> Version -> Parser (a -> a)
457#if MIN_VERSION_optparse_applicative(0,10,0)
458versioner prog ver = nullOption disabled $ mconcat
459#else
460versioner prog ver = nullOption $ mconcat
461#endif
462 [ long "version"
463 , help "Show program version and exit"
464 , value id
465 , metavar ""
466 , hidden
467 , mempty -- reader $ const $ undefined -- Left $ ErrorMsg versionStr
468 ]
469 where
470 versionStr = prog ++ " version " ++ showVersion ver
471
472parserInfo :: ParserInfo Options
473parserInfo = info parser modifier
474 where
475 parser = helper <*> versioner "mktorrent" version <*> options
476 modifier = header synopsis <> progDesc description <> fullDesc
477 synopsis = "Torrent management utility"
478 description = "" -- TODO
479
480{-----------------------------------------------------------------------
481-- Dispatch
482-----------------------------------------------------------------------}
483
484run :: Command -> IO ()
485run (Amend opts) = amend opts
486run (Check opts) = checkTorrent opts
487--run (Create opts) = createTorrent opts
488run (Get opts) = getTorrent opts
489run (Magnet opts) = magnet opts
490run (Show opts) = putTorrent opts
491
492prepare :: GlobalOpts -> IO ()
493prepare GlobalOpts {..} = do
494 updateGlobalLogger rootLoggerName (setLevel verbosity)
495
496main :: IO ()
497main = do
498 Options {..} <- execParser parserInfo
499 prepare globOpts
500 run cmdOpts
diff --git a/bittorrent/res/dapper-dvd-amd64.iso.torrent b/bittorrent/res/dapper-dvd-amd64.iso.torrent
new file mode 100644
index 00000000..5713344b
--- /dev/null
+++ b/bittorrent/res/dapper-dvd-amd64.iso.torrent
Binary files differ
diff --git a/bittorrent/res/pkg.torrent b/bittorrent/res/pkg.torrent
new file mode 100644
index 00000000..be89e9e0
--- /dev/null
+++ b/bittorrent/res/pkg.torrent
Binary files differ
diff --git a/bittorrent/res/testfile b/bittorrent/res/testfile
new file mode 100644
index 00000000..8e984818
--- /dev/null
+++ b/bittorrent/res/testfile
Binary files differ
diff --git a/bittorrent/res/testfile.torrent b/bittorrent/res/testfile.torrent
new file mode 100644
index 00000000..297f56a2
--- /dev/null
+++ b/bittorrent/res/testfile.torrent
@@ -0,0 +1 @@
d8:announce44:udp://tracker.openbittorrent.com:80/announce10:created by26:Enhanced-CTorrent/dnh3.3.213:creation datei1387753787e4:infod6:lengthi8192e4:name8:testfile12:piece lengthi262144e6:pieces20:œd•Dú—uÈÔtÝ®aÿöK³2e5:nodesll21:router.bittorrent.comi6881eeee \ No newline at end of file
diff --git a/bittorrent/src/Network/BitTorrent.hs b/bittorrent/src/Network/BitTorrent.hs
new file mode 100644
index 00000000..91a58887
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent.hs
@@ -0,0 +1,61 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8{-# LANGUAGE RecordWildCards #-}
9module Network.BitTorrent
10 ( -- * Client
11 Options (..)
12
13 -- ** Session
14 , Client
15 , clientPeerId
16 , clientListenerPort
17 , allowedExtensions
18
19 -- ** Initialization
20 , LogFun
21 , newClient
22 , closeClient
23 , withClient
24
25 -- ** Monadic
26 , MonadBitTorrent (..)
27 , BitTorrent
28 , runBitTorrent
29 , getClient
30 , simpleClient
31
32 -- * Torrent
33 -- ** Source
34 , InfoHash
35 , Magnet
36 , InfoDict
37 , Torrent
38
39 -- ** Handle
40 , Handle
41 , handleTopic
42 , handleTrackers
43 , handleExchange
44
45 , TorrentSource(openHandle)
46 , closeHandle
47 , getHandle
48 , getIndex
49
50 -- ** Control
51 , start
52 , pause
53 , stop
54
55 -- * Events
56 , EventSource (..)
57 ) where
58
59import Data.Torrent
60import Network.BitTorrent.Client
61import Network.BitTorrent.Internal.Types \ No newline at end of file
diff --git a/bittorrent/src/Network/BitTorrent/Client.hs b/bittorrent/src/Network/BitTorrent/Client.hs
new file mode 100644
index 00000000..c84290dd
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Client.hs
@@ -0,0 +1,195 @@
1{-# LANGUAGE FlexibleInstances #-}
2{-# LANGUAGE TypeSynonymInstances #-}
3{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4{-# LANGUAGE TemplateHaskell #-}
5module Network.BitTorrent.Client
6 ( -- * Options
7 Options (..)
8
9 -- * Client session
10 , Client
11
12 -- ** Session data
13 , clientPeerId
14 , clientListenerPort
15 , allowedExtensions
16
17 -- ** Session initialization
18 , LogFun
19 , newClient
20 , closeClient
21 , withClient
22 , simpleClient
23
24 -- * BitTorrent monad
25 , MonadBitTorrent (..)
26 , BitTorrent
27 , runBitTorrent
28 , getClient
29
30 -- * Handle
31 , Handle
32 , handleTopic
33 , handleTrackers
34 , handleExchange
35
36 -- ** Construction
37 , TorrentSource (..)
38 , closeHandle
39
40 -- ** Query
41 , getHandle
42 , getIndex
43
44 -- ** Management
45 , start
46 , pause
47 , stop
48 ) where
49
50import Control.Applicative
51import Control.Exception
52import Control.Concurrent
53import Control.Concurrent.Chan.Split as CS
54import Control.Monad.Logger
55import Control.Monad.Trans
56import Control.Monad.Trans.Resource
57
58import Data.Default
59import Data.HashMap.Strict as HM
60import Data.Text
61import Network
62
63import Data.Torrent
64import Network.Address
65import Network.BitTorrent.Client.Types
66import Network.BitTorrent.Client.Handle
67import Network.BitTorrent.DHT as DHT hiding (Options)
68import Network.BitTorrent.Tracker as Tracker hiding (Options)
69import Network.BitTorrent.Exchange as Exchange hiding (Options)
70import qualified Network.BitTorrent.Exchange as Exchange (Options(..))
71
72
73data Options = Options
74 { optFingerprint :: Fingerprint
75 , optName :: Text
76 , optPort :: PortNumber
77 , optExtensions :: [Extension]
78 , optNodeAddr :: NodeAddr IPv4
79 , optBootNode :: Maybe (NodeAddr IPv4)
80 }
81
82instance Default Options where
83 def = Options
84 { optFingerprint = def
85 , optName = "hs-bittorrent"
86 , optPort = 6882
87 , optExtensions = []
88 , optNodeAddr = "0.0.0.0:6882"
89 , optBootNode = Nothing
90 }
91
92exchangeOptions :: PeerId -> Options -> Exchange.Options
93exchangeOptions pid Options {..} = Exchange.Options
94 { optPeerAddr = PeerAddr (Just pid) (peerHost def) optPort
95 , optBacklog = optBacklog def
96 }
97
98connHandler :: MVar (HashMap InfoHash Handle) -> Exchange.Handler
99connHandler tmap ih = do
100 m <- readMVar tmap
101 case HM.lookup ih m of
102 Nothing -> error "torrent not found"
103 Just (Handle {..}) -> return handleExchange
104
105initClient :: Options -> LogFun -> ResIO Client
106initClient opts @ Options {..} logFun = do
107 pid <- liftIO genPeerId
108 tmap <- liftIO $ newMVar HM.empty
109
110 let peerInfo = PeerInfo pid Nothing optPort
111 let mkTracker = Tracker.newManager def peerInfo
112 (_, tmgr) <- allocate mkTracker Tracker.closeManager
113
114 let mkEx = Exchange.newManager (exchangeOptions pid opts) (connHandler tmap)
115 (_, emgr) <- allocate mkEx Exchange.closeManager
116
117 let mkNode = DHT.newNode defaultHandlers def optNodeAddr logFun Nothing
118 (_, node) <- allocate mkNode DHT.closeNode
119
120 resourceMap <- getInternalState
121 eventStream <- liftIO newSendPort
122
123 return Client
124 { clientPeerId = pid
125 , clientListenerPort = optPort
126 , allowedExtensions = toCaps optExtensions
127 , clientResources = resourceMap
128 , trackerManager = tmgr
129 , exchangeManager = emgr
130 , clientNode = node
131 , clientTorrents = tmap
132 , clientLogger = logFun
133 , clientEvents = eventStream
134 }
135
136newClient :: Options -> LogFun -> IO Client
137newClient opts logFun = do
138 s <- createInternalState
139 runInternalState (initClient opts logFun) s
140 `onException` closeInternalState s
141
142closeClient :: Client -> IO ()
143closeClient Client {..} = closeInternalState clientResources
144
145withClient :: Options -> LogFun -> (Client -> IO a) -> IO a
146withClient opts lf action = bracket (newClient opts lf) closeClient action
147
148-- do not perform IO in 'initClient', do it in the 'boot'
149--boot :: BitTorrent ()
150--boot = do
151-- Options {..} <- asks options
152-- liftDHT $ bootstrap (maybeToList optBootNode)
153
154-- | Run bittorrent client with default options and log to @stderr@.
155--
156-- For testing purposes only.
157--
158simpleClient :: BitTorrent () -> IO ()
159simpleClient m = do
160 runStderrLoggingT $ LoggingT $ \ logger -> do
161 withClient def logger (`runBitTorrent` m)
162
163{-----------------------------------------------------------------------
164-- Torrent identifiers
165-----------------------------------------------------------------------}
166
167class TorrentSource s where
168 openHandle :: FilePath -> s -> BitTorrent Handle
169
170instance TorrentSource InfoHash where
171 openHandle path ih = openMagnet path (nullMagnet ih)
172 {-# INLINE openHandle #-}
173
174instance TorrentSource Magnet where
175 openHandle = openMagnet
176 {-# INLINE openHandle #-}
177
178instance TorrentSource InfoDict where
179 openHandle path dict = openTorrent path (nullTorrent dict)
180 {-# INLINE openHandle #-}
181
182instance TorrentSource Torrent where
183 openHandle = openTorrent
184 {-# INLINE openHandle #-}
185
186instance TorrentSource FilePath where
187 openHandle contentDir torrentPath = do
188 t <- liftIO $ fromFile torrentPath
189 openTorrent contentDir t
190 {-# INLINE openHandle #-}
191
192getIndex :: BitTorrent [Handle]
193getIndex = do
194 Client {..} <- getClient
195 elems <$> liftIO (readMVar clientTorrents)
diff --git a/bittorrent/src/Network/BitTorrent/Client/Handle.hs b/bittorrent/src/Network/BitTorrent/Client/Handle.hs
new file mode 100644
index 00000000..66baac48
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Client/Handle.hs
@@ -0,0 +1,188 @@
1module Network.BitTorrent.Client.Handle
2 ( -- * Handle
3 Handle
4
5 -- * Initialization
6 , openTorrent
7 , openMagnet
8 , closeHandle
9
10 -- * Control
11 , start
12 , pause
13 , stop
14
15 -- * Query
16 , getHandle
17 , getStatus
18 ) where
19
20import Control.Concurrent.Chan.Split
21import Control.Concurrent.Lifted as L
22import Control.Monad
23import Control.Monad.Trans
24import Data.Default
25import Data.List as L
26import Data.HashMap.Strict as HM
27
28import Data.Torrent
29import Network.BitTorrent.Client.Types as Types
30import Network.BitTorrent.DHT as DHT
31import Network.BitTorrent.Exchange as Exchange
32import Network.BitTorrent.Tracker as Tracker
33
34{-----------------------------------------------------------------------
35-- Safe handle set manupulation
36-----------------------------------------------------------------------}
37
38allocHandle :: InfoHash -> BitTorrent Handle -> BitTorrent Handle
39allocHandle ih m = do
40 Client {..} <- getClient
41
42 (h, added) <- modifyMVar clientTorrents $ \ handles -> do
43 case HM.lookup ih handles of
44 Just h -> return (handles, (h, False))
45 Nothing -> do
46 h <- m
47 return (HM.insert ih h handles, (h, True))
48
49 when added $ do
50 liftIO $ send clientEvents (TorrentAdded ih)
51
52 return h
53
54freeHandle :: InfoHash -> BitTorrent () -> BitTorrent ()
55freeHandle ih finalizer = do
56 Client {..} <- getClient
57
58 modifyMVar_ clientTorrents $ \ handles -> do
59 case HM.lookup ih handles of
60 Nothing -> return handles
61 Just _ -> do
62 finalizer
63 return (HM.delete ih handles)
64
65lookupHandle :: InfoHash -> BitTorrent (Maybe Handle)
66lookupHandle ih = do
67 Client {..} <- getClient
68 handles <- readMVar clientTorrents
69 return (HM.lookup ih handles)
70
71{-----------------------------------------------------------------------
72-- Initialization
73-----------------------------------------------------------------------}
74
75newExchangeSession :: FilePath -> Either InfoHash InfoDict -> BitTorrent Exchange.Session
76newExchangeSession rootPath source = do
77 c @ Client {..} <- getClient
78 liftIO $ Exchange.newSession clientLogger (externalAddr c) rootPath source
79
80-- | Open a torrent in 'stop'ed state. Use 'nullTorrent' to open
81-- handle from 'InfoDict'. This operation do not block.
82openTorrent :: FilePath -> Torrent -> BitTorrent Handle
83openTorrent rootPath t @ Torrent {..} = do
84 let ih = idInfoHash tInfoDict
85 allocHandle ih $ do
86 statusVar <- newMVar Types.Stopped
87 tses <- liftIO $ Tracker.newSession ih (trackerList t)
88 eses <- newExchangeSession rootPath (Right tInfoDict)
89 eventStream <- liftIO newSendPort
90 return $ Handle
91 { handleTopic = ih
92 , handlePrivate = idPrivate tInfoDict
93 , handleStatus = statusVar
94 , handleTrackers = tses
95 , handleExchange = eses
96 , handleEvents = eventStream
97 }
98
99-- | Use 'nullMagnet' to open handle from 'InfoHash'.
100openMagnet :: FilePath -> Magnet -> BitTorrent Handle
101openMagnet rootPath Magnet {..} = do
102 allocHandle exactTopic $ do
103 statusVar <- newMVar Types.Stopped
104 tses <- liftIO $ Tracker.newSession exactTopic def
105 eses <- newExchangeSession rootPath (Left exactTopic)
106 eventStream <- liftIO newSendPort
107 return $ Handle
108 { handleTopic = exactTopic
109 , handlePrivate = False
110 , handleStatus = statusVar
111 , handleTrackers = tses
112 , handleExchange = eses
113 , handleEvents = eventStream
114 }
115
116-- | Stop torrent and destroy all sessions. You don't need to close
117-- handles at application exit, all handles will be automatically
118-- closed at 'Network.BitTorrent.Client.closeClient'. This operation
119-- may block.
120closeHandle :: Handle -> BitTorrent ()
121closeHandle h @ Handle {..} = do
122 freeHandle handleTopic $ do
123 Client {..} <- getClient
124 stop h
125 liftIO $ Exchange.closeSession handleExchange
126 liftIO $ Tracker.closeSession trackerManager handleTrackers
127
128{-----------------------------------------------------------------------
129-- Control
130-----------------------------------------------------------------------}
131
132modifyStatus :: HandleStatus -> Handle -> (HandleStatus -> BitTorrent ()) -> BitTorrent ()
133modifyStatus targetStatus Handle {..} targetAction = do
134 modifyMVar_ handleStatus $ \ actualStatus -> do
135 unless (actualStatus == targetStatus) $ do
136 targetAction actualStatus
137 return targetStatus
138 liftIO $ send handleEvents (StatusChanged targetStatus)
139
140-- | Start downloading, uploading and announcing this torrent.
141--
142-- This operation is blocking, use
143-- 'Control.Concurrent.Async.Lifted.async' if needed.
144start :: Handle -> BitTorrent ()
145start h @ Handle {..} = do
146 modifyStatus Types.Running h $ \ status -> do
147 case status of
148 Types.Running -> return ()
149 Types.Stopped -> do
150 Client {..} <- getClient
151 liftIO $ Tracker.notify trackerManager handleTrackers Tracker.Started
152 unless handlePrivate $ do
153 liftDHT $ DHT.insert handleTopic (error "start")
154 liftIO $ do
155 peers <- askPeers trackerManager handleTrackers
156 print $ "got: " ++ show (L.length peers) ++ " peers"
157 forM_ peers $ \ peer -> do
158 Exchange.connect peer handleExchange
159
160-- | Stop downloading this torrent.
161pause :: Handle -> BitTorrent ()
162pause _ = return ()
163
164-- | Stop downloading, uploading and announcing this torrent.
165stop :: Handle -> BitTorrent ()
166stop h @ Handle {..} = do
167 modifyStatus Types.Stopped h $ \ status -> do
168 case status of
169 Types.Stopped -> return ()
170 Types.Running -> do
171 Client {..} <- getClient
172 unless handlePrivate $ do
173 liftDHT $ DHT.delete handleTopic (error "stop")
174 liftIO $ Tracker.notify trackerManager handleTrackers Tracker.Stopped
175
176{-----------------------------------------------------------------------
177-- Query
178-----------------------------------------------------------------------}
179
180getHandle :: InfoHash -> BitTorrent Handle
181getHandle ih = do
182 mhandle <- lookupHandle ih
183 case mhandle of
184 Nothing -> error "should we throw some exception?"
185 Just h -> return h
186
187getStatus :: Handle -> IO HandleStatus
188getStatus Handle {..} = readMVar handleStatus
diff --git a/bittorrent/src/Network/BitTorrent/Client/Types.hs b/bittorrent/src/Network/BitTorrent/Client/Types.hs
new file mode 100644
index 00000000..e2ad858f
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Client/Types.hs
@@ -0,0 +1,163 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE TypeFamilies #-}
4{-# LANGUAGE MultiParamTypeClasses #-}
5{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Network.BitTorrent.Client.Types
7 ( -- * Core types
8 HandleStatus (..)
9 , Handle (..)
10 , Client (..)
11 , externalAddr
12
13 -- * Monad BitTorrent
14 , BitTorrent (..)
15 , runBitTorrent
16 , getClient
17
18 , MonadBitTorrent (..)
19
20 -- * Events
21 , Types.Event (..)
22 ) where
23
24import Control.Applicative
25import Control.Concurrent
26import Control.Concurrent.Chan.Split as CS
27import Control.Monad.Base
28import Control.Monad.Logger
29import Control.Monad.Reader
30import Control.Monad.Trans.Control
31import Control.Monad.Trans.Resource
32import Data.Function
33import Data.HashMap.Strict as HM
34import Data.Ord
35import Network
36import System.Log.FastLogger
37
38import Data.Torrent
39import Network.Address
40import Network.BitTorrent.Internal.Types as Types
41import Network.BitTorrent.DHT as DHT
42import Network.BitTorrent.Exchange as Exchange
43import Network.BitTorrent.Tracker as Tracker hiding (Event)
44
45data HandleStatus
46 = Running
47 | Stopped
48 deriving (Show, Eq)
49
50data Handle = Handle
51 { handleTopic :: !InfoHash
52 , handlePrivate :: !Bool
53
54 , handleStatus :: !(MVar HandleStatus)
55 , handleTrackers :: !Tracker.Session
56 , handleExchange :: !Exchange.Session
57 , handleEvents :: !(SendPort (Event Handle))
58 }
59
60instance EventSource Handle where
61 data Event Handle = StatusChanged HandleStatus
62 listen Handle {..} = CS.listen undefined
63
64data Client = Client
65 { clientPeerId :: !PeerId
66 , clientListenerPort :: !PortNumber
67 , allowedExtensions :: !Caps
68 , clientResources :: !InternalState
69 , trackerManager :: !Tracker.Manager
70 , exchangeManager :: !Exchange.Manager
71 , clientNode :: !(Node IPv4)
72 , clientTorrents :: !(MVar (HashMap InfoHash Handle))
73 , clientLogger :: !LogFun
74 , clientEvents :: !(SendPort (Event Client))
75 }
76
77instance Eq Client where
78 (==) = (==) `on` clientPeerId
79
80instance Ord Client where
81 compare = comparing clientPeerId
82
83instance EventSource Client where
84 data Event Client = TorrentAdded InfoHash
85 listen Client {..} = CS.listen clientEvents
86
87-- | External IP address of a host running a bittorrent client
88-- software may be used to acknowledge remote peer the host connected
89-- to. See 'Network.BitTorrent.Exchange.Message.ExtendedHandshake'.
90externalAddr :: Client -> PeerAddr (Maybe IP)
91externalAddr Client {..} = PeerAddr
92 { peerId = Just clientPeerId
93 , peerHost = Nothing -- TODO return external IP address, if known
94 , peerPort = clientListenerPort
95 }
96
97{-----------------------------------------------------------------------
98-- BitTorrent monad
99-----------------------------------------------------------------------}
100
101newtype BitTorrent a = BitTorrent
102 { unBitTorrent :: ReaderT Client IO a
103 } deriving ( Functor, Applicative, Monad
104 , MonadIO, MonadThrow, MonadBase IO
105 )
106
107class MonadBitTorrent m where
108 liftBT :: BitTorrent a -> m a
109
110#if MIN_VERSION_monad_control(1,0,0)
111newtype BTStM a = BTStM { unBTSt :: StM (ReaderT Client IO) a }
112
113instance MonadBaseControl IO BitTorrent where
114 type StM BitTorrent a = BTStM a
115 liftBaseWith cc = BitTorrent $ liftBaseWith $ \ cc' ->
116 cc $ \ (BitTorrent m) -> BTStM <$> cc' m
117 {-# INLINE liftBaseWith #-}
118
119 restoreM = BitTorrent . restoreM . unBTSt
120 {-# INLINE restoreM #-}
121#else
122instance MonadBaseControl IO BitTorrent where
123 newtype StM BitTorrent a = StM { unSt :: StM (ReaderT Client IO) a }
124 liftBaseWith cc = BitTorrent $ liftBaseWith $ \ cc' ->
125 cc $ \ (BitTorrent m) -> StM <$> cc' m
126 {-# INLINE liftBaseWith #-}
127
128 restoreM = BitTorrent . restoreM . unSt
129 {-# INLINE restoreM #-}
130#endif
131
132-- | NOP.
133instance MonadBitTorrent BitTorrent where
134 liftBT = id
135
136instance MonadTrans t => MonadBitTorrent (t BitTorrent) where
137 liftBT = lift
138
139-- | Registered but not closed manually resources will be
140-- automatically closed at 'Network.BitTorrent.Client.closeClient'
141instance MonadResource BitTorrent where
142 liftResourceT m = BitTorrent $ do
143 s <- asks clientResources
144 liftIO $ runInternalState m s
145
146-- | Run DHT operation, only if the client node is running.
147instance MonadDHT BitTorrent where
148 liftDHT action = BitTorrent $ do
149 node <- asks clientNode
150 liftIO $ runDHT node action
151
152instance MonadLogger BitTorrent where
153 monadLoggerLog loc src lvl msg = BitTorrent $ do
154 logger <- asks clientLogger
155 liftIO $ logger loc src lvl (toLogStr msg)
156
157runBitTorrent :: Client -> BitTorrent a -> IO a
158runBitTorrent client action = runReaderT (unBitTorrent action) client
159{-# INLINE runBitTorrent #-}
160
161getClient :: BitTorrent Client
162getClient = BitTorrent ask
163{-# INLINE getClient #-}
diff --git a/bittorrent/src/Network/BitTorrent/Exchange.hs b/bittorrent/src/Network/BitTorrent/Exchange.hs
new file mode 100644
index 00000000..143bf090
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange.hs
@@ -0,0 +1,35 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8module Network.BitTorrent.Exchange
9 ( -- * Manager
10 Options (..)
11 , Manager
12 , Handler
13 , newManager
14 , closeManager
15
16 -- * Session
17 , Caps
18 , Extension
19 , toCaps
20 , Session
21 , newSession
22 , closeSession
23
24 -- * Query
25 , waitMetadata
26 , takeMetadata
27
28 -- * Connections
29 , connect
30 , connectSink
31 ) where
32
33import Network.BitTorrent.Exchange.Manager
34import Network.BitTorrent.Exchange.Message
35import Network.BitTorrent.Exchange.Session
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs b/bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs
new file mode 100644
index 00000000..7bae3475
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs
@@ -0,0 +1,399 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This modules provides all necessary machinery to work with
9-- bitfields. Bitfields are used to keep track indices of complete
10-- pieces either this peer have or remote peer have.
11--
12-- There are also commonly used piece seletion algorithms
13-- which used to find out which one next piece to download.
14-- Selectors considered to be used in the following order:
15--
16-- * 'randomFirst' - at the start of download.
17--
18-- * 'rarestFirst' - performed to avoid situation when
19-- rarest piece is unaccessible.
20--
21-- * 'endGame' - performed after a peer has requested all
22-- the subpieces of the content.
23--
24-- Note that BitTorrent protocol recommend (TODO link?) the
25-- 'strictFirst' priority policy for /subpiece/ or /blocks/
26-- selection.
27--
28{-# LANGUAGE CPP #-}
29{-# LANGUAGE BangPatterns #-}
30{-# LANGUAGE RecordWildCards #-}
31module Network.BitTorrent.Exchange.Bitfield
32 ( -- * Bitfield
33 PieceIx
34 , PieceCount
35 , Bitfield
36
37 -- * Construction
38 , haveAll
39 , haveNone
40 , have
41 , singleton
42 , interval
43 , adjustSize
44
45 -- * Query
46 -- ** Cardinality
47 , Network.BitTorrent.Exchange.Bitfield.null
48 , Network.BitTorrent.Exchange.Bitfield.full
49 , haveCount
50 , totalCount
51 , completeness
52
53 -- ** Membership
54 , member
55 , notMember
56 , findMin
57 , findMax
58 , isSubsetOf
59
60 -- ** Availability
61 , complement
62 , Frequency
63 , frequencies
64 , rarest
65
66 -- * Combine
67 , insert
68 , union
69 , intersection
70 , difference
71
72 -- * Conversion
73 , toList
74 , fromList
75
76 -- * Serialization
77 , fromBitmap
78 , toBitmap
79
80 -- * Piece selection
81 , Selector
82 , selector
83 , strategyClass
84
85 , strictFirst
86 , strictLast
87 , rarestFirst
88 , randomFirst
89 , endGame
90 ) where
91
92import Control.Monad
93import Control.Monad.ST
94import Data.ByteString (ByteString)
95import qualified Data.ByteString as B
96import qualified Data.ByteString.Lazy as Lazy
97import Data.Vector.Unboxed (Vector)
98import qualified Data.Vector.Unboxed as V
99import qualified Data.Vector.Unboxed.Mutable as VM
100import Data.IntervalSet (IntSet)
101import qualified Data.IntervalSet as S
102import qualified Data.IntervalSet.ByteString as S
103import Data.List (foldl')
104import Data.Monoid
105import Data.Ratio
106
107import Data.Torrent
108
109-- TODO cache some operations
110
111-- | Bitfields are represented just as integer sets but with
112-- restriction: the each set should be within given interval (or
113-- subset of the specified interval). Size is used to specify
114-- interval, so bitfield of size 10 might contain only indices in
115-- interval [0..9].
116--
117data Bitfield = Bitfield {
118 bfSize :: !PieceCount
119 , bfSet :: !IntSet
120 } deriving (Show, Read, Eq)
121
122-- Invariants: all elements of bfSet lie in [0..bfSize - 1];
123
124instance Monoid Bitfield where
125 {-# SPECIALIZE instance Monoid Bitfield #-}
126 mempty = haveNone 0
127 mappend = union
128 mconcat = unions
129
130{-----------------------------------------------------------------------
131 Construction
132-----------------------------------------------------------------------}
133
134-- | The empty bitfield of the given size.
135haveNone :: PieceCount -> Bitfield
136haveNone s = Bitfield s S.empty
137
138-- | The full bitfield containing all piece indices for the given size.
139haveAll :: PieceCount -> Bitfield
140haveAll s = Bitfield s (S.interval 0 (s - 1))
141
142-- | Insert the index in the set ignoring out of range indices.
143have :: PieceIx -> Bitfield -> Bitfield
144have ix Bitfield {..}
145 | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet)
146 | otherwise = Bitfield bfSize bfSet
147
148singleton :: PieceIx -> PieceCount -> Bitfield
149singleton ix pc = have ix (haveNone pc)
150
151-- | Assign new size to bitfield. FIXME Normally, size should be only
152-- decreased, otherwise exception raised.
153adjustSize :: PieceCount -> Bitfield -> Bitfield
154adjustSize s Bitfield {..} = Bitfield s bfSet
155
156-- | NOTE: for internal use only
157interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield
158interval pc a b = Bitfield pc (S.interval a b)
159
160{-----------------------------------------------------------------------
161 Query
162-----------------------------------------------------------------------}
163
164-- | Test if bitifield have no one index: peer do not have anything.
165null :: Bitfield -> Bool
166null Bitfield {..} = S.null bfSet
167
168-- | Test if bitfield have all pieces.
169full :: Bitfield -> Bool
170full Bitfield {..} = S.size bfSet == bfSize
171
172-- | Count of peer have pieces.
173haveCount :: Bitfield -> PieceCount
174haveCount = S.size . bfSet
175
176-- | Total count of pieces and its indices.
177totalCount :: Bitfield -> PieceCount
178totalCount = bfSize
179
180-- | Ratio of /have/ piece count to the /total/ piece count.
181--
182-- > forall bf. 0 <= completeness bf <= 1
183--
184completeness :: Bitfield -> Ratio PieceCount
185completeness b = haveCount b % totalCount b
186
187inRange :: PieceIx -> Bitfield -> Bool
188inRange ix Bitfield {..} = 0 <= ix && ix < bfSize
189
190member :: PieceIx -> Bitfield -> Bool
191member ix bf @ Bitfield {..}
192 | ix `inRange` bf = ix `S.member` bfSet
193 | otherwise = False
194
195notMember :: PieceIx -> Bitfield -> Bool
196notMember ix bf @ Bitfield {..}
197 | ix `inRange` bf = ix `S.notMember` bfSet
198 | otherwise = True
199
200-- | Find first available piece index.
201findMin :: Bitfield -> PieceIx
202findMin = S.findMin . bfSet
203{-# INLINE findMin #-}
204
205-- | Find last available piece index.
206findMax :: Bitfield -> PieceIx
207findMax = S.findMax . bfSet
208{-# INLINE findMax #-}
209
210-- | Check if all pieces from first bitfield present if the second bitfield
211isSubsetOf :: Bitfield -> Bitfield -> Bool
212isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b
213{-# INLINE isSubsetOf #-}
214
215-- | Resulting bitfield includes only missing pieces.
216complement :: Bitfield -> Bitfield
217complement Bitfield {..} = Bitfield
218 { bfSet = uni `S.difference` bfSet
219 , bfSize = bfSize
220 }
221 where
222 Bitfield _ uni = haveAll bfSize
223{-# INLINE complement #-}
224
225{-----------------------------------------------------------------------
226-- Availability
227-----------------------------------------------------------------------}
228
229-- | Frequencies are needed in piece selection startegies which use
230-- availability quantity to find out the optimal next piece index to
231-- download.
232type Frequency = Int
233
234-- TODO rename to availability
235-- | How many times each piece index occur in the given bitfield set.
236frequencies :: [Bitfield] -> Vector Frequency
237frequencies [] = V.fromList []
238frequencies xs = runST $ do
239 v <- VM.new size
240 VM.set v 0
241 forM_ xs $ \ Bitfield {..} -> do
242 forM_ (S.toList bfSet) $ \ x -> do
243 fr <- VM.read v x
244 VM.write v x (succ fr)
245 V.unsafeFreeze v
246 where
247 size = maximum (map bfSize xs)
248
249-- TODO it seems like this operation is veeery slow
250
251-- | Find least available piece index. If no piece available return
252-- 'Nothing'.
253rarest :: [Bitfield] -> Maybe PieceIx
254rarest xs
255 | V.null freqMap = Nothing
256 | otherwise
257 = Just $ fst $ V.ifoldr' minIx (0, freqMap V.! 0) freqMap
258 where
259 freqMap = frequencies xs
260
261 minIx :: PieceIx -> Frequency
262 -> (PieceIx, Frequency)
263 -> (PieceIx, Frequency)
264 minIx ix fr acc@(_, fra)
265 | fr < fra && fr > 0 = (ix, fr)
266 | otherwise = acc
267
268
269{-----------------------------------------------------------------------
270 Combine
271-----------------------------------------------------------------------}
272
273insert :: PieceIx -> Bitfield -> Bitfield
274insert pix bf @ Bitfield {..}
275 | 0 <= pix && pix < bfSize = Bitfield
276 { bfSet = S.insert pix bfSet
277 , bfSize = bfSize
278 }
279 | otherwise = bf
280
281-- | Find indices at least one peer have.
282union :: Bitfield -> Bitfield -> Bitfield
283union a b = {-# SCC union #-} Bitfield {
284 bfSize = bfSize a `max` bfSize b
285 , bfSet = bfSet a `S.union` bfSet b
286 }
287
288-- | Find indices both peers have.
289intersection :: Bitfield -> Bitfield -> Bitfield
290intersection a b = {-# SCC intersection #-} Bitfield {
291 bfSize = bfSize a `min` bfSize b
292 , bfSet = bfSet a `S.intersection` bfSet b
293 }
294
295-- | Find indices which have first peer but do not have the second peer.
296difference :: Bitfield -> Bitfield -> Bitfield
297difference a b = {-# SCC difference #-} Bitfield {
298 bfSize = bfSize a -- FIXME is it reasonable?
299 , bfSet = bfSet a `S.difference` bfSet b
300 }
301
302-- | Find indices the any of the peers have.
303unions :: [Bitfield] -> Bitfield
304unions = {-# SCC unions #-} foldl' union (haveNone 0)
305
306{-----------------------------------------------------------------------
307 Serialization
308-----------------------------------------------------------------------}
309
310-- | List all /have/ indexes.
311toList :: Bitfield -> [PieceIx]
312toList Bitfield {..} = S.toList bfSet
313
314-- | Make bitfield from list of /have/ indexes.
315fromList :: PieceCount -> [PieceIx] -> Bitfield
316fromList s ixs = Bitfield {
317 bfSize = s
318 , bfSet = S.splitGT (-1) $ S.splitLT s $ S.fromList ixs
319 }
320
321-- | Unpack 'Bitfield' from tightly packed bit array. Note resulting
322-- size might be more than real bitfield size, use 'adjustSize'.
323fromBitmap :: ByteString -> Bitfield
324fromBitmap bs = {-# SCC fromBitmap #-} Bitfield {
325 bfSize = B.length bs * 8
326 , bfSet = S.fromByteString bs
327 }
328{-# INLINE fromBitmap #-}
329
330-- | Pack a 'Bitfield' to tightly packed bit array.
331toBitmap :: Bitfield -> Lazy.ByteString
332toBitmap Bitfield {..} = {-# SCC toBitmap #-} Lazy.fromChunks [intsetBM, alignment]
333 where
334 byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1
335 alignment = B.replicate (byteSize - B.length intsetBM) 0
336 intsetBM = S.toByteString bfSet
337
338{-----------------------------------------------------------------------
339-- Piece selection
340-----------------------------------------------------------------------}
341
342type Selector = Bitfield -- ^ Indices of client /have/ pieces.
343 -> Bitfield -- ^ Indices of peer /have/ pieces.
344 -> [Bitfield] -- ^ Indices of other peers /have/ pieces.
345 -> Maybe PieceIx -- ^ Zero-based index of piece to request
346 -- to, if any.
347
348selector :: Selector -- ^ Selector to use at the start.
349 -> Ratio PieceCount
350 -> Selector -- ^ Selector to use after the client have
351 -- the C pieces.
352 -> Selector -- ^ Selector that changes behaviour based
353 -- on completeness.
354selector start pt ready h a xs =
355 case strategyClass pt h of
356 SCBeginning -> start h a xs
357 SCReady -> ready h a xs
358 SCEnd -> endGame h a xs
359
360data StartegyClass
361 = SCBeginning
362 | SCReady
363 | SCEnd
364 deriving (Show, Eq, Ord, Enum, Bounded)
365
366
367strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass
368strategyClass threshold = classify . completeness
369 where
370 classify c
371 | c < threshold = SCBeginning
372 | c + 1 % numerator c < 1 = SCReady
373 -- FIXME numerator have is not total count
374 | otherwise = SCEnd
375
376
377-- | Select the first available piece.
378strictFirst :: Selector
379strictFirst h a _ = Just $ findMin (difference a h)
380
381-- | Select the last available piece.
382strictLast :: Selector
383strictLast h a _ = Just $ findMax (difference a h)
384
385-- |
386rarestFirst :: Selector
387rarestFirst h a xs = rarest (map (intersection want) xs)
388 where
389 want = difference h a
390
391-- | In average random first is faster than rarest first strategy but
392-- only if all pieces are available.
393randomFirst :: Selector
394randomFirst = do
395-- randomIO
396 error "randomFirst"
397
398endGame :: Selector
399endGame = strictLast
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Block.hs b/bittorrent/src/Network/BitTorrent/Exchange/Block.hs
new file mode 100644
index 00000000..bc9a3d24
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Block.hs
@@ -0,0 +1,369 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Blocks are used to transfer pieces.
9--
10{-# LANGUAGE BangPatterns #-}
11{-# LANGUAGE FlexibleInstances #-}
12{-# LANGUAGE TemplateHaskell #-}
13{-# LANGUAGE DeriveFunctor #-}
14{-# LANGUAGE DeriveDataTypeable #-}
15{-# LANGUAGE GeneralizedNewtypeDeriving #-}
16module Network.BitTorrent.Exchange.Block
17 ( -- * Block attributes
18 BlockOffset
19 , BlockCount
20 , BlockSize
21 , defaultTransferSize
22
23 -- * Block index
24 , BlockIx(..)
25 , blockIxRange
26
27 -- * Block data
28 , Block(..)
29 , blockIx
30 , blockSize
31 , blockRange
32 , isPiece
33 , leadingBlock
34
35 -- * Block bucket
36 , Bucket
37
38 -- ** Query
39 , Network.BitTorrent.Exchange.Block.null
40 , Network.BitTorrent.Exchange.Block.full
41 , Network.BitTorrent.Exchange.Block.size
42 , Network.BitTorrent.Exchange.Block.spans
43
44 -- ** Construction
45 , Network.BitTorrent.Exchange.Block.empty
46 , Network.BitTorrent.Exchange.Block.insert
47 , Network.BitTorrent.Exchange.Block.insertLazy
48 , Network.BitTorrent.Exchange.Block.merge
49 , Network.BitTorrent.Exchange.Block.fromList
50
51 -- ** Rendering
52 , Network.BitTorrent.Exchange.Block.toPiece
53
54 -- ** Debug
55 , Network.BitTorrent.Exchange.Block.valid
56 ) where
57
58import Prelude hiding (span)
59import Control.Applicative
60import Data.ByteString as BS hiding (span)
61import Data.ByteString.Lazy as BL hiding (span)
62import Data.ByteString.Lazy.Builder as BS
63import Data.Default
64import Data.Monoid
65import Data.List as L hiding (span)
66import Data.Serialize as S
67import Data.Typeable
68import Numeric
69import Text.PrettyPrint as PP hiding ((<>))
70import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
71
72import Data.Torrent
73
74{-----------------------------------------------------------------------
75-- Block attributes
76-----------------------------------------------------------------------}
77
78-- | Offset of a block in a piece in bytes. Should be multiple of
79-- the choosen block size.
80type BlockOffset = Int
81
82-- | Size of a block in bytes. Should be power of 2.
83--
84-- Normally block size is equal to 'defaultTransferSize'.
85--
86type BlockSize = Int
87
88-- | Number of block in a piece of a torrent. Used to distinguish
89-- block count from piece count.
90type BlockCount = Int
91
92-- | Widely used semi-official block size. Some clients can ignore if
93-- block size of BlockIx in Request message is not equal to this
94-- value.
95--
96defaultTransferSize :: BlockSize
97defaultTransferSize = 16 * 1024
98
99{-----------------------------------------------------------------------
100 Block Index
101-----------------------------------------------------------------------}
102
103-- | BlockIx correspond.
104data BlockIx = BlockIx {
105 -- | Zero-based piece index.
106 ixPiece :: {-# UNPACK #-} !PieceIx
107
108 -- | Zero-based byte offset within the piece.
109 , ixOffset :: {-# UNPACK #-} !BlockOffset
110
111 -- | Block size starting from offset.
112 , ixLength :: {-# UNPACK #-} !BlockSize
113 } deriving (Show, Eq, Typeable)
114
115-- | First block in torrent. Useful for debugging.
116instance Default BlockIx where
117 def = BlockIx 0 0 defaultTransferSize
118
119getInt :: S.Get Int
120getInt = fromIntegral <$> S.getWord32be
121{-# INLINE getInt #-}
122
123putInt :: S.Putter Int
124putInt = S.putWord32be . fromIntegral
125{-# INLINE putInt #-}
126
127instance Serialize BlockIx where
128 {-# SPECIALIZE instance Serialize BlockIx #-}
129 get = BlockIx <$> getInt
130 <*> getInt
131 <*> getInt
132 {-# INLINE get #-}
133
134 put BlockIx {..} = do
135 putInt ixPiece
136 putInt ixOffset
137 putInt ixLength
138 {-# INLINE put #-}
139
140instance Pretty BlockIx where
141 pPrint BlockIx {..} =
142 ("piece = " <> int ixPiece <> ",") <+>
143 ("offset = " <> int ixOffset <> ",") <+>
144 ("length = " <> int ixLength)
145
146-- | Get location of payload bytes in the torrent content.
147blockIxRange :: (Num a, Integral a) => PieceSize -> BlockIx -> (a, a)
148blockIxRange piSize BlockIx {..} = (offset, offset + len)
149 where
150 offset = fromIntegral piSize * fromIntegral ixPiece
151 + fromIntegral ixOffset
152 len = fromIntegral ixLength
153{-# INLINE blockIxRange #-}
154
155{-----------------------------------------------------------------------
156 Block
157-----------------------------------------------------------------------}
158
159data Block payload = Block {
160 -- | Zero-based piece index.
161 blkPiece :: {-# UNPACK #-} !PieceIx
162
163 -- | Zero-based byte offset within the piece.
164 , blkOffset :: {-# UNPACK #-} !BlockOffset
165
166 -- | Payload bytes.
167 , blkData :: !payload
168 } deriving (Show, Eq, Functor, Typeable)
169
170-- | Payload is ommitted.
171instance Pretty (Block BL.ByteString) where
172 pPrint = pPrint . blockIx
173 {-# INLINE pPrint #-}
174
175-- | Get size of block /payload/ in bytes.
176blockSize :: Block BL.ByteString -> BlockSize
177blockSize = fromIntegral . BL.length . blkData
178{-# INLINE blockSize #-}
179
180-- | Get block index of a block.
181blockIx :: Block BL.ByteString -> BlockIx
182blockIx = BlockIx <$> blkPiece <*> blkOffset <*> blockSize
183
184-- | Get location of payload bytes in the torrent content.
185blockRange :: (Num a, Integral a)
186 => PieceSize -> Block BL.ByteString -> (a, a)
187blockRange piSize = blockIxRange piSize . blockIx
188{-# INLINE blockRange #-}
189
190-- | Test if a block can be safely turned into a piece.
191isPiece :: PieceSize -> Block BL.ByteString -> Bool
192isPiece pieceLen blk @ (Block i offset _) =
193 offset == 0 && blockSize blk == pieceLen && i >= 0
194{-# INLINE isPiece #-}
195
196-- | First block in the piece.
197leadingBlock :: PieceIx -> BlockSize -> BlockIx
198leadingBlock pix blockSize = BlockIx
199 { ixPiece = pix
200 , ixOffset = 0
201 , ixLength = blockSize
202 }
203{-# INLINE leadingBlock #-}
204
205{-----------------------------------------------------------------------
206-- Bucket
207-----------------------------------------------------------------------}
208
209type Pos = Int
210type ChunkSize = Int
211
212-- | A sparse set of blocks used to represent an /in progress/ piece.
213data Bucket
214 = Nil
215 | Span {-# UNPACK #-} !ChunkSize !Bucket
216 | Fill {-# UNPACK #-} !ChunkSize !Builder !Bucket
217
218instance Show Bucket where
219 showsPrec i Nil = showString ""
220 showsPrec i (Span s xs) = showString "Span " <> showInt s
221 <> showString " " <> showsPrec i xs
222 showsPrec i (Fill s _ xs) = showString "Fill " <> showInt s
223 <> showString " " <> showsPrec i xs
224
225-- | INVARIANT: 'Nil' should appear only after 'Span' of 'Fill'.
226nilInvFailed :: a
227nilInvFailed = error "Nil: bucket invariant failed"
228
229valid :: Bucket -> Bool
230valid = check Nothing
231 where
232 check Nothing Nil = False -- see 'nilInvFailed'
233 check (Just _) _ = True
234 check prevIsSpan (Span sz xs) =
235 prevIsSpan /= Just True && -- Span n (NotSpan .. ) invariant
236 sz > 0 && -- Span is always non-empty
237 check (Just True) xs
238 check prevIsSpan (Fill sz b xs) =
239 prevIsSpan /= Just True && -- Fill n (NotFill .. ) invariant
240 sz > 0 && -- Fill is always non-empty
241 check (Just False) xs
242
243instance Pretty Bucket where
244 pPrint Nil = nilInvFailed
245 pPrint bkt = go bkt
246 where
247 go Nil = PP.empty
248 go (Span sz xs) = "Span" <+> PP.int sz <+> go xs
249 go (Fill sz b xs) = "Fill" <+> PP.int sz <+> go xs
250
251-- | Smart constructor: use it when some block is /deleted/ from
252-- bucket.
253span :: ChunkSize -> Bucket -> Bucket
254span sz (Span sz' xs) = Span (sz + sz') xs
255span sz xxs = Span sz xxs
256{-# INLINE span #-}
257
258-- | Smart constructor: use it when some block is /inserted/ to
259-- bucket.
260fill :: ChunkSize -> Builder -> Bucket -> Bucket
261fill sz b (Fill sz' b' xs) = Fill (sz + sz') (b <> b') xs
262fill sz b xxs = Fill sz b xxs
263{-# INLINE fill #-}
264
265{-----------------------------------------------------------------------
266-- Bucket queries
267-----------------------------------------------------------------------}
268
269-- | /O(1)/. Test if this bucket is empty.
270null :: Bucket -> Bool
271null Nil = nilInvFailed
272null (Span _ Nil) = True
273null _ = False
274{-# INLINE null #-}
275
276-- | /O(1)/. Test if this bucket is complete.
277full :: Bucket -> Bool
278full Nil = nilInvFailed
279full (Fill _ _ Nil) = True
280full _ = False
281{-# INLINE full #-}
282
283-- | /O(n)/. Total size of the incompleted piece.
284size :: Bucket -> PieceSize
285size Nil = nilInvFailed
286size bkt = go bkt
287 where
288 go Nil = 0
289 go (Span sz xs) = sz + go xs
290 go (Fill sz _ xs) = sz + go xs
291
292-- | /O(n)/. List incomplete blocks to download. If some block have
293-- size more than the specified 'BlockSize' then block is split into
294-- smaller blocks to satisfy given 'BlockSize'. Small (for
295-- e.g. trailing) blocks is not ignored, but returned in-order.
296spans :: BlockSize -> Bucket -> [(BlockOffset, BlockSize)]
297spans expectedSize = go 0
298 where
299 go _ Nil = []
300 go off (Span sz xs) = listChunks off sz ++ go (off + sz) xs
301 go off (Fill sz _ xs) = go (off + sz) xs
302
303 listChunks off restSize
304 | restSize <= 0 = []
305 | otherwise = (off, blkSize)
306 : listChunks (off + blkSize) (restSize - blkSize)
307 where
308 blkSize = min expectedSize restSize
309
310{-----------------------------------------------------------------------
311-- Bucket contstruction
312-----------------------------------------------------------------------}
313
314-- | /O(1)/. A new empty bucket capable to alloof specified size.
315empty :: PieceSize -> Bucket
316empty sz
317 | sz < 0 = error "empty: Bucket size must be a non-negative value"
318 | otherwise = Span sz Nil
319{-# INLINE empty #-}
320
321insertSpan :: Pos -> BS.ByteString -> ChunkSize -> Bucket -> Bucket
322insertSpan !pos !bs !span_sz !xs =
323 let pref_len = pos
324 fill_len = span_sz - pos `min` BS.length bs
325 suff_len = (span_sz - pos) - fill_len
326 in mkSpan pref_len $
327 fill fill_len (byteString (BS.take fill_len bs)) $
328 mkSpan suff_len $
329 xs
330 where
331 mkSpan 0 xs = xs
332 mkSpan sz xs = Span sz xs
333
334-- | /O(n)/. Insert a strict bytestring at specified position.
335--
336-- Best case: if blocks are inserted in sequential order, then this
337-- operation should take /O(1)/.
338--
339insert :: Pos -> BS.ByteString -> Bucket -> Bucket
340insert _ _ Nil = nilInvFailed
341insert dstPos bs bucket = go 0 bucket
342 where
343 intersects curPos sz = dstPos >= curPos && dstPos <= curPos + sz
344
345 go _ Nil = Nil
346 go curPos (Span sz xs)
347 | intersects curPos sz = insertSpan (dstPos - curPos) bs sz xs
348 | otherwise = span sz (go (curPos + sz) xs)
349 go curPos bkt @ (Fill sz br xs)
350 | intersects curPos sz = bkt
351 | otherwise = fill sz br (go (curPos + sz) xs)
352
353fromList :: PieceSize -> [(Pos, BS.ByteString)] -> Bucket
354fromList s = L.foldr (uncurry Network.BitTorrent.Exchange.Block.insert)
355 (Network.BitTorrent.Exchange.Block.empty s)
356
357-- TODO zero-copy
358insertLazy :: Pos -> BL.ByteString -> Bucket -> Bucket
359insertLazy pos bl = Network.BitTorrent.Exchange.Block.insert pos (BL.toStrict bl)
360
361-- | /O(n)/.
362merge :: Bucket -> Bucket -> Bucket
363merge = error "Bucket.merge: not implemented"
364
365-- | /O(1)/.
366toPiece :: Bucket -> Maybe BL.ByteString
367toPiece Nil = nilInvFailed
368toPiece (Fill _ b Nil) = Just (toLazyByteString b)
369toPiece _ = Nothing
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Connection.hs b/bittorrent/src/Network/BitTorrent/Exchange/Connection.hs
new file mode 100644
index 00000000..6804d0a2
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Connection.hs
@@ -0,0 +1,1012 @@
1-- |
2-- Module : Network.BitTorrent.Exchange.Wire
3-- Copyright : (c) Sam Truzjan 2013
4-- (c) Daniel Gröber 2013
5-- License : BSD3
6-- Maintainer : pxqr.sta@gmail.com
7-- Stability : experimental
8-- Portability : portable
9--
10-- Each peer wire connection is identified by triple @(topic,
11-- remote_addr, this_addr)@. This means that connections are the
12-- same if and only if their 'ConnectionId' are the same. Of course,
13-- you /must/ avoid duplicated connections.
14--
15-- This module control /integrity/ of data send and received.
16--
17{-# LANGUAGE DeriveDataTypeable #-}
18{-# LANGUAGE TemplateHaskell #-}
19{-# LANGUAGE MultiParamTypeClasses #-}
20{-# LANGUAGE GeneralizedNewtypeDeriving #-}
21module Network.BitTorrent.Exchange.Connection
22 ( -- * Wire
23 Connected
24 , Wire
25 , ChannelSide (..)
26
27 -- * Connection
28 , Connection
29 , connInitiatedBy
30
31 -- ** Identity
32 , connRemoteAddr
33 , connTopic
34 , connRemotePeerId
35 , connThisPeerId
36
37 -- ** Capabilities
38 , connProtocol
39 , connCaps
40 , connExtCaps
41 , connRemoteEhs
42
43 -- ** State
44 , connStatus
45 , connBitfield
46
47 -- ** Env
48 , connOptions
49 , connSession
50 , connStats
51
52 -- ** Status
53 , PeerStatus (..)
54 , ConnectionStatus (..)
55 , updateStatus
56 , statusUpdates
57 , clientStatus
58 , remoteStatus
59 , canUpload
60 , canDownload
61 , defaultUnchokeSlots
62 , defaultRechokeInterval
63
64
65 -- * Setup
66 , ConnectionPrefs (..)
67 , SessionLink (..)
68 , ConnectionConfig (..)
69
70 -- ** Initiate
71 , connectWire
72
73 -- ** Accept
74 , PendingConnection
75 , newPendingConnection
76 , pendingPeer
77 , pendingCaps
78 , pendingTopic
79 , closePending
80 , acceptWire
81
82 -- ** Post setup actions
83 , resizeBitfield
84
85 -- * Messaging
86 , recvMessage
87 , sendMessage
88 , filterQueue
89 , getMaxQueueLength
90
91 -- * Exceptions
92 , ProtocolError (..)
93 , WireFailure (..)
94 , peerPenalty
95 , isWireFailure
96 , disconnectPeer
97
98 -- * Stats
99 , ByteStats (..)
100 , FlowStats (..)
101 , ConnectionStats (..)
102
103 -- * Flood detection
104 , FloodDetector (..)
105
106 -- * Options
107 , Options (..)
108 ) where
109
110import Control.Applicative
111import Control.Concurrent hiding (yield)
112import Control.Exception
113import Control.Monad.Reader
114import Control.Monad.State
115import Control.Monad.Trans.Resource
116import Control.Lens
117import Data.ByteString as BS
118import Data.ByteString.Lazy as BSL
119import Data.Conduit as C
120import Data.Conduit.Cereal
121import Data.Conduit.List
122import Data.Conduit.Network
123import Data.Default
124import Data.IORef
125import Data.List as L
126import Data.Maybe as M
127import Data.Monoid
128import Data.Serialize as S
129import Data.Typeable
130import Network
131import Network.Socket hiding (Connected)
132import Network.Socket.ByteString as BS
133import Text.PrettyPrint as PP hiding ((<>))
134import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
135import Text.Show.Functions ()
136import System.Log.FastLogger (ToLogStr(..))
137import System.Timeout
138
139import Data.Torrent
140import Network.Address
141import Network.BitTorrent.Exchange.Bitfield as BF
142import Network.BitTorrent.Exchange.Message as Msg
143
144-- TODO handle port message?
145-- TODO handle limits?
146-- TODO filter not requested PIECE messages
147-- TODO metadata piece request flood protection
148-- TODO piece request flood protection
149-- TODO protect against flood attacks
150{-----------------------------------------------------------------------
151-- Exceptions
152-----------------------------------------------------------------------}
153
154-- | Used to specify initiator of 'ProtocolError'.
155data ChannelSide
156 = ThisPeer
157 | RemotePeer
158 deriving (Show, Eq, Enum, Bounded)
159
160instance Default ChannelSide where
161 def = ThisPeer
162
163instance Pretty ChannelSide where
164 pPrint = PP.text . show
165
166-- | A protocol errors occur when a peer violates protocol
167-- specification.
168data ProtocolError
169 -- | Protocol string should be 'BitTorrent Protocol' but remote
170 -- peer have sent a different string.
171 = InvalidProtocol ProtocolName
172
173 -- | Sent and received protocol strings do not match. Can occur
174 -- in 'connectWire' only.
175 | UnexpectedProtocol ProtocolName
176
177 -- | /Remote/ peer replied with invalid 'hsInfoHash' which do not
178 -- match with 'hsInfoHash' /this/ peer have sent. Can occur in
179 -- 'connectWire' or 'acceptWire' only.
180 | UnexpectedTopic InfoHash
181
182 -- | Some trackers or DHT can return 'PeerId' of a peer. If a
183 -- remote peer handshaked with different 'hsPeerId' then this
184 -- exception is raised. Can occur in 'connectWire' only.
185 | UnexpectedPeerId PeerId
186
187 -- | Accepted peer have sent unknown torrent infohash in
188 -- 'hsInfoHash' field. This situation usually happen when /this/
189 -- peer have deleted the requested torrent. The error can occur in
190 -- 'acceptWire' function only.
191 | UnknownTopic InfoHash
192
193 -- | A remote peer have 'ExtExtended' enabled but did not send an
194 -- 'ExtendedHandshake' back.
195 | HandshakeRefused
196
197 -- | 'Network.BitTorrent.Exchange.Message.Bitfield' message MUST
198 -- be send either once or zero times, but either this peer or
199 -- remote peer send a bitfield message the second time.
200 | BitfieldAlreadySent ChannelSide
201
202 -- | Capabilities violation. For example this exception can occur
203 -- when a peer have sent 'Port' message but 'ExtDHT' is not
204 -- allowed in 'connCaps'.
205 | DisallowedMessage
206 { -- | Who sent invalid message.
207 violentSender :: ChannelSide
208
209 -- | If the 'violentSender' reconnect with this extension
210 -- enabled then he can try to send this message.
211 , extensionRequired :: Extension
212 }
213 deriving Show
214
215instance Pretty ProtocolError where
216 pPrint = PP.text . show
217
218errorPenalty :: ProtocolError -> Int
219errorPenalty (InvalidProtocol _) = 1
220errorPenalty (UnexpectedProtocol _) = 1
221errorPenalty (UnexpectedTopic _) = 1
222errorPenalty (UnexpectedPeerId _) = 1
223errorPenalty (UnknownTopic _) = 0
224errorPenalty (HandshakeRefused ) = 1
225errorPenalty (BitfieldAlreadySent _) = 1
226errorPenalty (DisallowedMessage _ _) = 1
227
228-- | Exceptions used to interrupt the current P2P session.
229data WireFailure
230 = ConnectionRefused IOError
231
232 -- | Force termination of wire connection.
233 --
234 -- Normally you should throw only this exception from event loop
235 -- using 'disconnectPeer', other exceptions are thrown
236 -- automatically by functions from this module.
237 --
238 | DisconnectPeer
239
240 -- | A peer not responding and did not send a 'KeepAlive' message
241 -- for a specified period of time.
242 | PeerDisconnected
243
244 -- | A remote peer have sent some unknown message we unable to
245 -- parse.
246 | DecodingError GetException
247
248 -- | See 'ProtocolError' for more details.
249 | ProtocolError ProtocolError
250
251 -- | A possible malicious peer have sent too many control messages
252 -- without making any progress.
253 | FloodDetected ConnectionStats
254 deriving (Show, Typeable)
255
256instance Exception WireFailure
257
258instance Pretty WireFailure where
259 pPrint = PP.text . show
260
261-- TODO
262-- data Penalty = Ban | Penalty Int
263
264peerPenalty :: WireFailure -> Int
265peerPenalty DisconnectPeer = 0
266peerPenalty PeerDisconnected = 0
267peerPenalty (DecodingError _) = 1
268peerPenalty (ProtocolError e) = errorPenalty e
269peerPenalty (FloodDetected _) = 1
270
271-- | Do nothing with exception, used with 'handle' or 'try'.
272isWireFailure :: Monad m => WireFailure -> m ()
273isWireFailure _ = return ()
274
275protocolError :: MonadThrow m => ProtocolError -> m a
276protocolError = monadThrow . ProtocolError
277
278{-----------------------------------------------------------------------
279-- Stats
280-----------------------------------------------------------------------}
281
282-- | Message stats in one direction.
283data FlowStats = FlowStats
284 { -- | Number of the messages sent or received.
285 messageCount :: {-# UNPACK #-} !Int
286 -- | Sum of byte sequences of all messages.
287 , messageBytes :: {-# UNPACK #-} !ByteStats
288 } deriving Show
289
290instance Pretty FlowStats where
291 pPrint FlowStats {..} =
292 PP.int messageCount <+> "messages" $+$
293 pPrint messageBytes
294
295-- | Zeroed stats.
296instance Default FlowStats where
297 def = FlowStats 0 def
298
299-- | Monoid under addition.
300instance Monoid FlowStats where
301 mempty = def
302 mappend a b = FlowStats
303 { messageBytes = messageBytes a <> messageBytes b
304 , messageCount = messageCount a + messageCount b
305 }
306
307-- | Find average length of byte sequences per message.
308avgByteStats :: FlowStats -> ByteStats
309avgByteStats (FlowStats n ByteStats {..}) = ByteStats
310 { overhead = overhead `quot` n
311 , control = control `quot` n
312 , payload = payload `quot` n
313 }
314
315-- | Message stats in both directions. This data can be retrieved
316-- using 'getStats' function.
317--
318-- Note that this stats is completely different from
319-- 'Data.Torrent.Progress.Progress': payload bytes not necessary
320-- equal to downloaded\/uploaded bytes since a peer can send a
321-- broken block.
322--
323data ConnectionStats = ConnectionStats
324 { -- | Received messages stats.
325 incomingFlow :: !FlowStats
326 -- | Sent messages stats.
327 , outcomingFlow :: !FlowStats
328 } deriving Show
329
330instance Pretty ConnectionStats where
331 pPrint ConnectionStats {..} = vcat
332 [ "Recv:" <+> pPrint incomingFlow
333 , "Sent:" <+> pPrint outcomingFlow
334 , "Both:" <+> pPrint (incomingFlow <> outcomingFlow)
335 ]
336
337-- | Zeroed stats.
338instance Default ConnectionStats where
339 def = ConnectionStats def def
340
341-- | Monoid under addition.
342instance Monoid ConnectionStats where
343 mempty = def
344 mappend a b = ConnectionStats
345 { incomingFlow = incomingFlow a <> incomingFlow b
346 , outcomingFlow = outcomingFlow a <> outcomingFlow b
347 }
348
349-- | Aggregate one more message stats in the /specified/ direction.
350addStats :: ChannelSide -> ByteStats -> ConnectionStats -> ConnectionStats
351addStats ThisPeer x s = s { outcomingFlow = (FlowStats 1 x) <> (outcomingFlow s) }
352addStats RemotePeer x s = s { incomingFlow = (FlowStats 1 x) <> (incomingFlow s) }
353
354-- | Sum of overhead and control bytes in both directions.
355wastedBytes :: ConnectionStats -> Int
356wastedBytes ConnectionStats {..} = overhead + control
357 where
358 FlowStats _ ByteStats {..} = incomingFlow <> outcomingFlow
359
360-- | Sum of payload bytes in both directions.
361payloadBytes :: ConnectionStats -> Int
362payloadBytes ConnectionStats {..} =
363 payload (messageBytes (incomingFlow <> outcomingFlow))
364
365-- | Sum of any bytes in both directions.
366transmittedBytes :: ConnectionStats -> Int
367transmittedBytes ConnectionStats {..} =
368 byteLength (messageBytes (incomingFlow <> outcomingFlow))
369
370{-----------------------------------------------------------------------
371-- Flood protection
372-----------------------------------------------------------------------}
373
374defaultFloodFactor :: Int
375defaultFloodFactor = 1
376
377-- | This is a very permissive value, connection setup usually takes
378-- around 10-100KB, including both directions.
379defaultFloodThreshold :: Int
380defaultFloodThreshold = 2 * 1024 * 1024
381
382-- | A flood detection function.
383type Detector stats = Int -- ^ Factor;
384 -> Int -- ^ Threshold;
385 -> stats -- ^ Stats to analyse;
386 -> Bool -- ^ Is this a flooded connection?
387
388defaultDetector :: Detector ConnectionStats
389defaultDetector factor threshold s =
390 transmittedBytes s > threshold &&
391 factor * wastedBytes s > payloadBytes s
392
393-- | Flood detection is used to protect /this/ peer against a /remote/
394-- malicious peer sending meaningless control messages.
395data FloodDetector = FloodDetector
396 { -- | Max ratio of payload bytes to control bytes.
397 floodFactor :: {-# UNPACK #-} !Int
398
399 -- | Max count of bytes connection /setup/ can take including
400 -- 'Handshake', 'ExtendedHandshake', 'Bitfield', 'Have' and 'Port'
401 -- messages. This value is used to avoid false positives at the
402 -- connection initialization.
403 , floodThreshold :: {-# UNPACK #-} !Int
404
405 -- | Flood predicate on the /current/ 'ConnectionStats'.
406 , floodPredicate :: Detector ConnectionStats
407 } deriving Show
408
409instance Eq FloodDetector where
410 a == b = floodFactor a == floodFactor b
411 && floodThreshold a == floodThreshold b
412
413-- | Flood detector with very permissive options.
414instance Default FloodDetector where
415 def = FloodDetector
416 { floodFactor = defaultFloodFactor
417 , floodThreshold = defaultFloodThreshold
418 , floodPredicate = defaultDetector
419 }
420
421-- | This peer might drop connection if the detector gives positive answer.
422runDetector :: FloodDetector -> ConnectionStats -> Bool
423runDetector FloodDetector {..} = floodPredicate floodFactor floodThreshold
424
425{-----------------------------------------------------------------------
426-- Options
427-----------------------------------------------------------------------}
428
429-- | Various connection settings and limits.
430data Options = Options
431 { -- | How often /this/ peer should send 'KeepAlive' messages.
432 keepaliveInterval :: {-# UNPACK #-} !Int
433
434 -- | /This/ peer will drop connection if a /remote/ peer did not
435 -- send any message for this period of time.
436 , keepaliveTimeout :: {-# UNPACK #-} !Int
437
438 , requestQueueLength :: {-# UNPACK #-} !Int
439
440 -- | Used to protect against flood attacks.
441 , floodDetector :: FloodDetector
442
443 -- | Used to protect against flood attacks in /metadata
444 -- exchange/. Normally, a requesting peer should request each
445 -- 'InfoDict' piece only one time, but a malicious peer can
446 -- saturate wire with 'MetadataRequest' messages thus flooding
447 -- responding peer.
448 --
449 -- This value set upper bound for number of 'MetadataRequests'
450 -- for each piece.
451 --
452 , metadataFactor :: {-# UNPACK #-} !Int
453
454 -- | Used to protect against out-of-memory attacks: malicious peer
455 -- can claim that 'totalSize' is, say, 100TB and send some random
456 -- data instead of infodict pieces. Since requesting peer unable
457 -- to check not completed infodict via the infohash, the
458 -- accumulated pieces will allocate the all available memory.
459 --
460 -- This limit set upper bound for 'InfoDict' size. See
461 -- 'ExtendedMetadata' for more info.
462 --
463 , maxInfoDictSize :: {-# UNPACK #-} !Int
464 } deriving (Show, Eq)
465
466-- | Permissive default parameters, most likely you don't need to
467-- change them.
468instance Default Options where
469 def = Options
470 { keepaliveInterval = defaultKeepAliveInterval
471 , keepaliveTimeout = defaultKeepAliveTimeout
472 , requestQueueLength = defaultRequestQueueLength
473 , floodDetector = def
474 , metadataFactor = defaultMetadataFactor
475 , maxInfoDictSize = defaultMaxInfoDictSize
476 }
477
478{-----------------------------------------------------------------------
479-- Peer status
480-----------------------------------------------------------------------}
481
482-- | Connections contain two bits of state on either end: choked or
483-- not, and interested or not.
484data PeerStatus = PeerStatus
485 { -- | Choking is a notification that no data will be sent until
486 -- unchoking happens.
487 _choking :: !Bool
488
489 -- |
490 , _interested :: !Bool
491 } deriving (Show, Eq, Ord)
492
493$(makeLenses ''PeerStatus)
494
495instance Pretty PeerStatus where
496 pPrint PeerStatus {..} =
497 pPrint (Choking _choking) <+> "and" <+> pPrint (Interested _interested)
498
499-- | Connections start out choked and not interested.
500instance Default PeerStatus where
501 def = PeerStatus True False
502
503instance Monoid PeerStatus where
504 mempty = def
505 mappend a b = PeerStatus
506 { _choking = _choking a && _choking b
507 , _interested = _interested a || _interested b
508 }
509
510-- | Can be used to update remote peer status using incoming 'Status'
511-- message.
512updateStatus :: StatusUpdate -> PeerStatus -> PeerStatus
513updateStatus (Choking b) = choking .~ b
514updateStatus (Interested b) = interested .~ b
515
516-- | Can be used to generate outcoming messages.
517statusUpdates :: PeerStatus -> PeerStatus -> [StatusUpdate]
518statusUpdates a b = M.catMaybes $
519 [ if _choking a == _choking b then Nothing
520 else Just $ Choking $ _choking b
521 , if _interested a == _interested b then Nothing
522 else Just $ Interested $ _interested b
523 ]
524
525{-----------------------------------------------------------------------
526-- Connection status
527-----------------------------------------------------------------------}
528
529-- | Status of the both endpoints.
530data ConnectionStatus = ConnectionStatus
531 { _clientStatus :: !PeerStatus
532 , _remoteStatus :: !PeerStatus
533 } deriving (Show, Eq)
534
535$(makeLenses ''ConnectionStatus)
536
537instance Pretty ConnectionStatus where
538 pPrint ConnectionStatus {..} =
539 "this " PP.<+> pPrint _clientStatus PP.$$
540 "remote" PP.<+> pPrint _remoteStatus
541
542-- | Connections start out choked and not interested.
543instance Default ConnectionStatus where
544 def = ConnectionStatus def def
545
546-- | Can the client transfer to the remote peer?
547canUpload :: ConnectionStatus -> Bool
548canUpload ConnectionStatus {..}
549 = _interested _remoteStatus && not (_choking _clientStatus)
550
551-- | Can the client transfer from the remote peer?
552canDownload :: ConnectionStatus -> Bool
553canDownload ConnectionStatus {..}
554 = _interested _clientStatus && not (_choking _remoteStatus)
555
556-- | Indicates how many peers are allowed to download from the client
557-- by default.
558defaultUnchokeSlots :: Int
559defaultUnchokeSlots = 4
560
561-- |
562defaultRechokeInterval :: Int
563defaultRechokeInterval = 10 * 1000 * 1000
564
565{-----------------------------------------------------------------------
566-- Connection
567-----------------------------------------------------------------------}
568
569data ConnectionState = ConnectionState {
570 -- | If @not (allowed ExtExtended connCaps)@ then this set is always
571 -- empty. Otherwise it has the BEP10 extension protocol mandated mapping of
572 -- 'MessageId' to the message type for the remote peer.
573 --
574 -- Note that this value can change in current session if either
575 -- this or remote peer will initiate rehandshaking.
576 --
577 _connExtCaps :: !ExtendedCaps
578
579 -- | Current extended handshake information from the remote peer
580 , _connRemoteEhs :: !ExtendedHandshake
581
582 -- | Various stats about messages sent and received. Stats can be
583 -- used to protect /this/ peer against flood attacks.
584 --
585 -- Note that this value will change with the next sent or received
586 -- message.
587 , _connStats :: !ConnectionStats
588
589 , _connStatus :: !ConnectionStatus
590
591 -- | Bitfield of remote endpoint.
592 , _connBitfield :: !Bitfield
593 }
594
595makeLenses ''ConnectionState
596
597instance Default ConnectionState where
598 def = ConnectionState
599 { _connExtCaps = def
600 , _connRemoteEhs = def
601 , _connStats = def
602 , _connStatus = def
603 , _connBitfield = BF.haveNone 0
604 }
605
606-- | Connection keep various info about both peers.
607data Connection s = Connection
608 { connInitiatedBy :: !ChannelSide
609
610 , connRemoteAddr :: !(PeerAddr IP)
611
612 -- | /Both/ peers handshaked with this protocol string. The only
613 -- value is \"Bittorrent Protocol\" but this can be changed in
614 -- future.
615 , connProtocol :: !ProtocolName
616
617 -- | Set of enabled core extensions, i.e. the pre BEP10 extension
618 -- mechanism. This value is used to check if a message is allowed
619 -- to be sent or received.
620 , connCaps :: !Caps
621
622 -- | /Both/ peers handshaked with this infohash. A connection can
623 -- handle only one topic, use 'reconnect' to change the current
624 -- topic.
625 , connTopic :: !InfoHash
626
627 -- | Typically extracted from handshake.
628 , connRemotePeerId :: !PeerId
629
630 -- | Typically extracted from handshake.
631 , connThisPeerId :: !PeerId
632
633 -- |
634 , connOptions :: !Options
635
636 -- | Mutable connection state, see 'ConnectionState'
637 , connState :: !(IORef ConnectionState)
638
639-- -- | Max request queue length.
640-- , connMaxQueueLen :: !Int
641
642 -- | Environment data.
643 , connSession :: !s
644
645 , connChan :: !(Chan Message)
646 }
647
648instance Pretty (Connection s) where
649 pPrint Connection {..} = "Connection"
650
651instance ToLogStr (Connection s) where
652 toLogStr Connection {..} = mconcat
653 [ toLogStr (show connRemoteAddr)
654 , toLogStr (show connProtocol)
655 , toLogStr (show connCaps)
656 , toLogStr (show connTopic)
657 , toLogStr (show connRemotePeerId)
658 , toLogStr (show connThisPeerId)
659 , toLogStr (show connOptions)
660 ]
661
662-- TODO check extended messages too
663isAllowed :: Connection s -> Message -> Bool
664isAllowed Connection {..} msg
665 | Just ext <- requires msg = ext `allowed` connCaps
666 | otherwise = True
667
668{-----------------------------------------------------------------------
669-- Hanshaking
670-----------------------------------------------------------------------}
671
672sendHandshake :: Socket -> Handshake -> IO ()
673sendHandshake sock hs = sendAll sock (S.encode hs)
674
675recvHandshake :: Socket -> IO Handshake
676recvHandshake sock = do
677 header <- BS.recv sock 1
678 unless (BS.length header == 1) $
679 throw $ userError "Unable to receive handshake header."
680
681 let protocolLen = BS.head header
682 let restLen = handshakeSize protocolLen - 1
683
684 body <- BS.recv sock restLen
685 let resp = BS.cons protocolLen body
686 either (throwIO . userError) return $ S.decode resp
687
688-- | Handshaking with a peer specified by the second argument.
689--
690-- It's important to send handshake first because /accepting/ peer
691-- do not know handshake topic and will wait until /connecting/ peer
692-- will send handshake.
693--
694initiateHandshake :: Socket -> Handshake -> IO Handshake
695initiateHandshake sock hs = do
696 sendHandshake sock hs
697 recvHandshake sock
698
699data HandshakePair = HandshakePair
700 { handshakeSent :: !Handshake
701 , handshakeRecv :: !Handshake
702 } deriving (Show, Eq)
703
704validatePair :: HandshakePair -> PeerAddr IP -> IO ()
705validatePair (HandshakePair hs hs') addr = Prelude.mapM_ checkProp
706 [ (def == hsProtocol hs', InvalidProtocol $ hsProtocol hs')
707 , (hsProtocol hs == hsProtocol hs', UnexpectedProtocol $ hsProtocol hs')
708 , (hsInfoHash hs == hsInfoHash hs', UnexpectedTopic $ hsInfoHash hs')
709 , (hsPeerId hs' == fromMaybe (hsPeerId hs') (peerId addr)
710 , UnexpectedPeerId $ hsPeerId hs')
711 ]
712 where
713 checkProp (t, e) = unless t $ throwIO $ ProtocolError e
714
715-- | Connection state /right/ after handshaking.
716establishedStats :: HandshakePair -> ConnectionStats
717establishedStats HandshakePair {..} = ConnectionStats
718 { outcomingFlow = FlowStats 1 $ handshakeStats handshakeSent
719 , incomingFlow = FlowStats 1 $ handshakeStats handshakeRecv
720 }
721
722{-----------------------------------------------------------------------
723-- Wire
724-----------------------------------------------------------------------}
725
726-- | do not expose this so we can change it without breaking api
727newtype Connected s a = Connected { runConnected :: (ReaderT (Connection s) IO a) }
728 deriving (Functor, Applicative, Monad
729 , MonadIO, MonadReader (Connection s), MonadThrow
730 )
731
732instance MonadState ConnectionState (Connected s) where
733 get = Connected (asks connState) >>= liftIO . readIORef
734 put x = Connected (asks connState) >>= liftIO . flip writeIORef x
735
736-- | A duplex channel connected to a remote peer which keep tracks
737-- connection parameters.
738type Wire s a = ConduitM Message Message (Connected s) a
739
740{-----------------------------------------------------------------------
741-- Wrapper
742-----------------------------------------------------------------------}
743
744putStats :: ChannelSide -> Message -> Connected s ()
745putStats side msg = connStats %= addStats side (stats msg)
746
747validate :: ChannelSide -> Message -> Connected s ()
748validate side msg = do
749 caps <- asks connCaps
750 case requires msg of
751 Nothing -> return ()
752 Just ext
753 | ext `allowed` caps -> return ()
754 | otherwise -> protocolError $ DisallowedMessage side ext
755
756trackFlow :: ChannelSide -> Wire s ()
757trackFlow side = iterM $ do
758 validate side
759 putStats side
760
761{-----------------------------------------------------------------------
762-- Setup
763-----------------------------------------------------------------------}
764
765-- System.Timeout.timeout multiplier
766seconds :: Int
767seconds = 1000000
768
769sinkChan :: MonadIO m => Chan Message -> Sink Message m ()
770sinkChan chan = await >>= maybe (return ()) (liftIO . writeChan chan)
771
772sourceChan :: MonadIO m => Int -> Chan Message -> Source m Message
773sourceChan interval chan = do
774 mmsg <- liftIO $ timeout (interval * seconds) $ readChan chan
775 yield $ fromMaybe Msg.KeepAlive mmsg
776
777-- | Normally you should use 'connectWire' or 'acceptWire'.
778runWire :: Wire s () -> Socket -> Chan Message -> Connection s -> IO ()
779runWire action sock chan conn = flip runReaderT conn $ runConnected $
780 sourceSocket sock $=
781 conduitGet S.get $=
782 trackFlow RemotePeer $=
783 action $=
784 trackFlow ThisPeer C.$$
785 sinkChan chan
786
787-- | This function will block until a peer send new message. You can
788-- also use 'await'.
789recvMessage :: Wire s Message
790recvMessage = await >>= maybe (monadThrow PeerDisconnected) return
791
792-- | You can also use 'yield'.
793sendMessage :: PeerMessage msg => msg -> Wire s ()
794sendMessage msg = do
795 ecaps <- use connExtCaps
796 yield $ envelop ecaps msg
797
798getMaxQueueLength :: Connected s Int
799getMaxQueueLength = do
800 advertisedLen <- ehsQueueLength <$> use connRemoteEhs
801 defaultLen <- asks (requestQueueLength . connOptions)
802 return $ fromMaybe defaultLen advertisedLen
803
804-- | Filter pending messages from send buffer.
805filterQueue :: (Message -> Bool) -> Wire s ()
806filterQueue p = lift $ do
807 chan <- asks connChan
808 liftIO $ getChanContents chan >>= writeList2Chan chan . L.filter p
809
810-- | Forcefully terminate wire session and close socket.
811disconnectPeer :: Wire s a
812disconnectPeer = monadThrow DisconnectPeer
813
814extendedHandshake :: ExtendedCaps -> Wire s ()
815extendedHandshake caps = do
816 -- TODO add other params to the handshake
817 sendMessage $ nullExtendedHandshake caps
818 msg <- recvMessage
819 case msg of
820 Extended (EHandshake remoteEhs@(ExtendedHandshake {..})) -> do
821 connExtCaps .= (ehsCaps <> caps)
822 connRemoteEhs .= remoteEhs
823 _ -> protocolError HandshakeRefused
824
825rehandshake :: ExtendedCaps -> Wire s ()
826rehandshake caps = error "rehandshake"
827
828reconnect :: Wire s ()
829reconnect = error "reconnect"
830
831data ConnectionId = ConnectionId
832 { topic :: !InfoHash
833 , remoteAddr :: !(PeerAddr IP)
834 , thisAddr :: !(PeerAddr (Maybe IP)) -- ^ foreign address of this node.
835 }
836
837-- | /Preffered/ settings of wire. To get the real use 'ask'.
838data ConnectionPrefs = ConnectionPrefs
839 { prefOptions :: !Options
840 , prefProtocol :: !ProtocolName
841 , prefCaps :: !Caps
842 , prefExtCaps :: !ExtendedCaps
843 } deriving (Show, Eq)
844
845instance Default ConnectionPrefs where
846 def = ConnectionPrefs
847 { prefOptions = def
848 , prefProtocol = def
849 , prefCaps = def
850 , prefExtCaps = def
851 }
852
853normalize :: ConnectionPrefs -> ConnectionPrefs
854normalize = error "normalize"
855
856-- | Bridge between 'Connection' and 'Network.BitTorrent.Exchange.Session'.
857data SessionLink s = SessionLink
858 { linkTopic :: !(InfoHash)
859 , linkPeerId :: !(PeerId)
860 , linkMetadataSize :: !(Maybe Int)
861 , linkOutputChan :: !(Maybe (Chan Message))
862 , linkSession :: !(s)
863 }
864
865data ConnectionConfig s = ConnectionConfig
866 { cfgPrefs :: !(ConnectionPrefs)
867 , cfgSession :: !(SessionLink s)
868 , cfgWire :: !(Wire s ())
869 }
870
871configHandshake :: ConnectionConfig s -> Handshake
872configHandshake ConnectionConfig {..} = Handshake
873 { hsProtocol = prefProtocol cfgPrefs
874 , hsReserved = prefCaps cfgPrefs
875 , hsInfoHash = linkTopic cfgSession
876 , hsPeerId = linkPeerId cfgSession
877 }
878
879{-----------------------------------------------------------------------
880-- Pending connections
881-----------------------------------------------------------------------}
882
883-- | Connection in half opened state. A normal usage scenario:
884--
885-- * Opened using 'newPendingConnection', usually in the listener
886-- loop;
887--
888-- * Closed using 'closePending' if 'pendingPeer' is banned,
889-- 'pendingCaps' is prohibited or pendingTopic is unknown;
890--
891-- * Accepted using 'acceptWire' otherwise.
892--
893data PendingConnection = PendingConnection
894 { pendingSock :: Socket
895 , pendingPeer :: PeerAddr IP -- ^ 'peerId' is always non empty;
896 , pendingCaps :: Caps -- ^ advertised by the peer;
897 , pendingTopic :: InfoHash -- ^ possible non-existent topic.
898 }
899
900-- | Reconstruct handshake sent by the remote peer.
901pendingHandshake :: PendingConnection -> Handshake
902pendingHandshake PendingConnection {..} = Handshake
903 { hsProtocol = def
904 , hsReserved = pendingCaps
905 , hsInfoHash = pendingTopic
906 , hsPeerId = fromMaybe (error "pendingHandshake: impossible")
907 (peerId pendingPeer)
908 }
909
910-- |
911--
912-- This function can throw 'WireFailure' exception.
913--
914newPendingConnection :: Socket -> PeerAddr IP -> IO PendingConnection
915newPendingConnection sock addr = do
916 Handshake {..} <- recvHandshake sock
917 unless (hsProtocol == def) $ do
918 throwIO $ ProtocolError $ InvalidProtocol hsProtocol
919 return PendingConnection
920 { pendingSock = sock
921 , pendingPeer = addr { peerId = Just hsPeerId }
922 , pendingCaps = hsReserved
923 , pendingTopic = hsInfoHash
924 }
925
926-- | Release all resources associated with the given connection. Note
927-- that you /must not/ 'closePending' if you 'acceptWire'.
928closePending :: PendingConnection -> IO ()
929closePending PendingConnection {..} = do
930 close pendingSock
931
932{-----------------------------------------------------------------------
933-- Connection setup
934-----------------------------------------------------------------------}
935
936chanToSock :: Int -> Chan Message -> Socket -> IO ()
937chanToSock ka chan sock =
938 sourceChan ka chan $= conduitPut S.put C.$$ sinkSocket sock
939
940afterHandshaking :: ChannelSide -> PeerAddr IP -> Socket -> HandshakePair
941 -> ConnectionConfig s -> IO ()
942afterHandshaking initiator addr sock
943 hpair @ (HandshakePair hs hs')
944 (ConnectionConfig
945 { cfgPrefs = ConnectionPrefs {..}
946 , cfgSession = SessionLink {..}
947 , cfgWire = wire
948 }) = do
949 let caps = hsReserved hs <> hsReserved hs'
950 cstate <- newIORef def { _connStats = establishedStats hpair }
951 chan <- maybe newChan return linkOutputChan
952 let conn = Connection {
953 connInitiatedBy = initiator
954 , connRemoteAddr = addr
955 , connProtocol = hsProtocol hs
956 , connCaps = caps
957 , connTopic = hsInfoHash hs
958 , connRemotePeerId = hsPeerId hs'
959 , connThisPeerId = hsPeerId hs
960 , connOptions = def
961 , connState = cstate
962 , connSession = linkSession
963 , connChan = chan
964 }
965
966 -- TODO make KA interval configurable
967 let kaInterval = defaultKeepAliveInterval
968 wire' = if ExtExtended `allowed` caps
969 then extendedHandshake prefExtCaps >> wire
970 else wire
971
972 bracket (forkIO (chanToSock kaInterval chan sock))
973 (killThread)
974 (\ _ -> runWire wire' sock chan conn)
975
976-- | Initiate 'Wire' connection and handshake with a peer. This function will
977-- also do the BEP10 extension protocol handshake if 'ExtExtended' is enabled on
978-- both sides.
979--
980-- This function can throw 'WireFailure' exception.
981--
982connectWire :: PeerAddr IP -> ConnectionConfig s -> IO ()
983connectWire addr cfg = do
984 let catchRefusal m = try m >>= either (throwIO . ConnectionRefused) return
985 bracket (catchRefusal (peerSocket Stream addr)) close $ \ sock -> do
986 let hs = configHandshake cfg
987 hs' <- initiateHandshake sock hs
988 let hpair = HandshakePair hs hs'
989 validatePair hpair addr
990 afterHandshaking ThisPeer addr sock hpair cfg
991
992-- | Accept 'Wire' connection using already 'Network.Socket.accept'ed
993-- socket. For peer listener loop the 'acceptSafe' should be
994-- prefered against 'accept'. The socket will be closed at exit.
995--
996-- This function can throw 'WireFailure' exception.
997--
998acceptWire :: PendingConnection -> ConnectionConfig s -> IO ()
999acceptWire pc @ PendingConnection {..} cfg = do
1000 bracket (return pendingSock) close $ \ _ -> do
1001 unless (linkTopic (cfgSession cfg) == pendingTopic) $ do
1002 throwIO (ProtocolError (UnexpectedTopic pendingTopic))
1003
1004 let hs = configHandshake cfg
1005 sendHandshake pendingSock hs
1006 let hpair = HandshakePair hs (pendingHandshake pc)
1007
1008 afterHandshaking RemotePeer pendingPeer pendingSock hpair cfg
1009
1010-- | Used when size of bitfield becomes known.
1011resizeBitfield :: Int -> Connected s ()
1012resizeBitfield n = connBitfield %= adjustSize n
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Download.hs b/bittorrent/src/Network/BitTorrent/Exchange/Download.hs
new file mode 100644
index 00000000..981db2fb
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Download.hs
@@ -0,0 +1,296 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8--
9--
10{-# LANGUAGE FlexibleContexts #-}
11{-# LANGUAGE FlexibleInstances #-}
12{-# LANGUAGE MultiParamTypeClasses #-}
13{-# LANGUAGE FunctionalDependencies #-}
14{-# LANGUAGE TemplateHaskell #-}
15module Network.BitTorrent.Exchange.Download
16 ( -- * Downloading
17 Download (..)
18 , Updates
19 , runDownloadUpdates
20
21 -- ** Metadata
22 -- $metadata-download
23 , MetadataDownload
24 , metadataDownload
25
26 -- ** Content
27 -- $content-download
28 , ContentDownload
29 , contentDownload
30 ) where
31
32import Control.Applicative
33import Control.Concurrent
34import Control.Lens
35import Control.Monad.State
36import Data.BEncode as BE
37import Data.ByteString as BS
38import Data.ByteString.Lazy as BL
39import Data.Default
40import Data.List as L
41import Data.Maybe
42import Data.Map as M
43import Data.Tuple
44
45import Data.Torrent as Torrent
46import Network.Address
47import Network.BitTorrent.Exchange.Bitfield as BF
48import Network.BitTorrent.Exchange.Block as Block
49import Network.BitTorrent.Exchange.Message as Msg
50import System.Torrent.Storage (Storage, writePiece)
51
52
53{-----------------------------------------------------------------------
54-- Class
55-----------------------------------------------------------------------}
56
57type Updates s a = StateT s IO a
58
59runDownloadUpdates :: MVar s -> Updates s a -> IO a
60runDownloadUpdates var m = modifyMVar var (fmap swap . runStateT m)
61
62class Download s chunk | s -> chunk where
63 scheduleBlocks :: Int -> PeerAddr IP -> Bitfield -> Updates s [BlockIx]
64
65 -- |
66 scheduleBlock :: PeerAddr IP -> Bitfield -> Updates s (Maybe BlockIx)
67 scheduleBlock addr bf = listToMaybe <$> scheduleBlocks 1 addr bf
68
69 -- | Get number of sent requests to this peer.
70 getRequestQueueLength :: PeerAddr IP -> Updates s Int
71
72 -- | Remove all pending block requests to the remote peer. May be used
73 -- when:
74 --
75 -- * a peer closes connection;
76 --
77 -- * remote peer choked this peer;
78 --
79 -- * timeout expired.
80 --
81 resetPending :: PeerAddr IP -> Updates s ()
82
83 -- | MAY write to storage, if a new piece have been completed.
84 --
85 -- You should check if a returned by peer block is actually have
86 -- been requested and in-flight. This is needed to avoid "I send
87 -- random corrupted block" attacks.
88 pushBlock :: PeerAddr IP -> chunk -> Updates s (Maybe Bool)
89
90{-----------------------------------------------------------------------
91-- Metadata download
92-----------------------------------------------------------------------}
93-- $metadata-download
94-- TODO
95
96data MetadataDownload = MetadataDownload
97 { _pendingPieces :: [(PeerAddr IP, PieceIx)]
98 , _bucket :: Bucket
99 , _topic :: InfoHash
100 }
101
102makeLenses ''MetadataDownload
103
104-- | Create a new scheduler for infodict of the given size.
105metadataDownload :: Int -> InfoHash -> MetadataDownload
106metadataDownload ps = MetadataDownload [] (Block.empty ps)
107
108instance Default MetadataDownload where
109 def = error "instance Default MetadataDownload"
110
111--cancelPending :: PieceIx -> Updates ()
112cancelPending pix = pendingPieces %= L.filter ((pix ==) . snd)
113
114instance Download MetadataDownload (Piece BS.ByteString) where
115 scheduleBlock addr bf = do
116 bkt <- use bucket
117 case spans metadataPieceSize bkt of
118 [] -> return Nothing
119 ((off, _ ) : _) -> do
120 let pix = off `div` metadataPieceSize
121 pendingPieces %= ((addr, pix) :)
122 return (Just (BlockIx pix 0 metadataPieceSize))
123
124 resetPending addr = pendingPieces %= L.filter ((addr ==) . fst)
125
126 pushBlock addr Torrent.Piece {..} = do
127 p <- use pendingPieces
128 when ((addr, pieceIndex) `L.notElem` p) $
129 error "not requested"
130 cancelPending pieceIndex
131
132 bucket %= Block.insert (metadataPieceSize * pieceIndex) pieceData
133 b <- use bucket
134 case toPiece b of
135 Nothing -> return Nothing
136 Just chunks -> do
137 t <- use topic
138 case parseInfoDict (BL.toStrict chunks) t of
139 Right x -> do
140 pendingPieces .= []
141 return undefined -- (Just x)
142 Left e -> do
143 pendingPieces .= []
144 bucket .= Block.empty (Block.size b)
145 return undefined -- Nothing
146 where
147 -- todo use incremental parsing to avoid BS.concat call
148 parseInfoDict :: BS.ByteString -> InfoHash -> Result InfoDict
149 parseInfoDict chunk topic =
150 case BE.decode chunk of
151 Right (infodict @ InfoDict {..})
152 | topic == idInfoHash -> return infodict
153 | otherwise -> Left "broken infodict"
154 Left err -> Left $ "unable to parse infodict " ++ err
155
156{-----------------------------------------------------------------------
157-- Content download
158-----------------------------------------------------------------------}
159-- $content-download
160--
161-- A block can have one of the following status:
162--
163-- 1) /not allowed/: Piece is not in download set.
164--
165-- 2) /waiting/: (allowed?) Block have been allowed to download,
166-- but /this/ peer did not send any 'Request' message for this
167-- block. To allow some piece use
168-- 'Network.BitTorrent.Exchange.Selector' and then 'allowedSet'
169-- and 'allowPiece'.
170--
171-- 3) /inflight/: (pending?) Block have been requested but
172-- /remote/ peer did not send any 'Piece' message for this block.
173-- Related functions 'markInflight'
174--
175-- 4) /pending/: (stalled?) Block have have been downloaded
176-- Related functions 'insertBlock'.
177--
178-- Piece status:
179--
180-- 1) /assembled/: (downloaded?) All blocks in piece have been
181-- downloaded but the piece did not verified yet.
182--
183-- * Valid: go to completed;
184--
185-- * Invalid: go to waiting.
186--
187-- 2) /corrupted/:
188--
189-- 3) /downloaded/: (verified?) A piece have been successfully
190-- verified via the hash. Usually the piece should be stored to
191-- the 'System.Torrent.Storage' and /this/ peer should send 'Have'
192-- messages to the /remote/ peers.
193--
194
195data PieceEntry = PieceEntry
196 { pending :: [(PeerAddr IP, BlockIx)]
197 , stalled :: Bucket
198 }
199
200pieceEntry :: PieceSize -> PieceEntry
201pieceEntry s = PieceEntry [] (Block.empty s)
202
203isEmpty :: PieceEntry -> Bool
204isEmpty PieceEntry {..} = L.null pending && Block.null stalled
205
206_holes :: PieceIx -> PieceEntry -> [BlockIx]
207_holes pix PieceEntry {..} = fmap mkBlockIx (spans defaultTransferSize stalled)
208 where
209 mkBlockIx (off, sz) = BlockIx pix off sz
210
211data ContentDownload = ContentDownload
212 { inprogress :: !(Map PieceIx PieceEntry)
213 , bitfield :: !Bitfield
214 , pieceSize :: !PieceSize
215 , contentStorage :: Storage
216 }
217
218contentDownload :: Bitfield -> PieceSize -> Storage -> ContentDownload
219contentDownload = ContentDownload M.empty
220
221--modifyEntry :: PieceIx -> (PieceEntry -> PieceEntry) -> DownloadUpdates ()
222modifyEntry pix f = modify $ \ s @ ContentDownload {..} -> s
223 { inprogress = alter (g pieceSize) pix inprogress }
224 where
225 g s = h . f . fromMaybe (pieceEntry s)
226 h e
227 | isEmpty e = Nothing
228 | otherwise = Just e
229
230instance Download ContentDownload (Block BL.ByteString) where
231 scheduleBlocks n addr maskBF = do
232 ContentDownload {..} <- get
233 let wantPieces = maskBF `BF.difference` bitfield
234 let wantBlocks = L.concat $ M.elems $ M.mapWithKey _holes $
235 M.filterWithKey (\ pix _ -> pix `BF.member` wantPieces)
236 inprogress
237
238 bixs <- if L.null wantBlocks
239 then do
240 mpix <- choosePiece wantPieces
241 case mpix of -- TODO return 'n' blocks
242 Nothing -> return []
243 Just pix -> return [leadingBlock pix defaultTransferSize]
244 else chooseBlocks wantBlocks n
245
246 forM_ bixs $ \ bix -> do
247 modifyEntry (ixPiece bix) $ \ e @ PieceEntry {..} -> e
248 { pending = (addr, bix) : pending }
249
250 return bixs
251 where
252 -- TODO choose block nearest to pending or stalled sets to reduce disk
253 -- seeks on remote machines
254 --chooseBlocks :: [BlockIx] -> Int -> DownloadUpdates [BlockIx]
255 chooseBlocks xs n = return (L.take n xs)
256
257 -- TODO use selection strategies from Exchange.Selector
258 --choosePiece :: Bitfield -> DownloadUpdates (Maybe PieceIx)
259 choosePiece bf
260 | BF.null bf = return $ Nothing
261 | otherwise = return $ Just $ BF.findMin bf
262
263 getRequestQueueLength addr = do
264 m <- gets (M.map (L.filter ((==) addr . fst) . pending) . inprogress)
265 return $ L.sum $ L.map L.length $ M.elems m
266
267 resetPending addr = modify $ \ s -> s { inprogress = reset (inprogress s) }
268 where
269 reset = fmap $ \ e -> e
270 { pending = L.filter (not . (==) addr . fst) (pending e) }
271
272 pushBlock addr blk @ Block {..} = do
273 mpe <- gets (M.lookup blkPiece . inprogress)
274 case mpe of
275 Nothing -> return Nothing
276 Just (pe @ PieceEntry {..})
277 | blockIx blk `L.notElem` fmap snd pending -> return Nothing
278 | otherwise -> do
279 let bkt' = Block.insertLazy blkOffset blkData stalled
280 case toPiece bkt' of
281 Nothing -> do
282 modifyEntry blkPiece $ \ e @ PieceEntry {..} -> e
283 { pending = L.filter ((==) (blockIx blk) . snd) pending
284 , stalled = bkt'
285 }
286 return (Just False)
287
288 Just pieceData -> do
289 -- TODO verify
290 storage <- gets contentStorage
291 liftIO $ writePiece (Torrent.Piece blkPiece pieceData) storage
292 modify $ \ s @ ContentDownload {..} -> s
293 { inprogress = M.delete blkPiece inprogress
294 , bitfield = BF.insert blkPiece bitfield
295 }
296 return (Just True)
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Manager.hs b/bittorrent/src/Network/BitTorrent/Exchange/Manager.hs
new file mode 100644
index 00000000..30a6a607
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Manager.hs
@@ -0,0 +1,62 @@
1module Network.BitTorrent.Exchange.Manager
2 ( Options (..)
3 , Manager
4 , Handler
5 , newManager
6 , closeManager
7 ) where
8
9import Control.Concurrent
10import Control.Exception hiding (Handler)
11import Control.Monad
12import Data.Default
13import Network.Socket
14
15import Data.Torrent
16import Network.Address
17import Network.BitTorrent.Exchange.Connection hiding (Options)
18import Network.BitTorrent.Exchange.Session
19
20
21data Options = Options
22 { optBacklog :: Int
23 , optPeerAddr :: PeerAddr IP
24 } deriving (Show, Eq)
25
26instance Default Options where
27 def = Options
28 { optBacklog = maxListenQueue
29 , optPeerAddr = def
30 }
31
32data Manager = Manager
33 { listener :: !ThreadId
34 }
35
36type Handler = InfoHash -> IO Session
37
38handleNewConn :: Socket -> PeerAddr IP -> Handler -> IO ()
39handleNewConn sock addr handler = do
40 conn <- newPendingConnection sock addr
41 ses <- handler (pendingTopic conn) `onException` closePending conn
42 establish conn ses
43
44listenIncoming :: Options -> Handler -> IO ()
45listenIncoming Options {..} handler = do
46 bracket (socket AF_INET Stream defaultProtocol) close $ \ sock -> do
47 bind sock (toSockAddr optPeerAddr)
48 listen sock optBacklog
49 forever $ do
50 (conn, sockAddr) <- accept sock
51 case fromSockAddr sockAddr of
52 Nothing -> return ()
53 Just addr -> void $ forkIO $ handleNewConn sock addr handler
54
55newManager :: Options -> Handler -> IO Manager
56newManager opts handler = do
57 tid <- forkIO $ listenIncoming opts handler
58 return (Manager tid)
59
60closeManager :: Manager -> IO ()
61closeManager Manager {..} = do
62 killThread listener \ No newline at end of file
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Message.hs b/bittorrent/src/Network/BitTorrent/Exchange/Message.hs
new file mode 100644
index 00000000..2c6770f7
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Message.hs
@@ -0,0 +1,1232 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Normally peer to peer communication consisting of the following
9-- steps:
10--
11-- * In order to establish the connection between peers we should
12-- send 'Handshake' message. The 'Handshake' is a required message
13-- and must be the first message transmitted by the peer to the
14-- another peer. Another peer should reply with a handshake as well.
15--
16-- * Next peer might sent bitfield message, but might not. In the
17-- former case we should update bitfield peer have. Again, if we
18-- have some pieces we should send bitfield. Normally bitfield
19-- message should sent after the handshake message.
20--
21-- * Regular exchange messages. TODO docs
22--
23-- For more high level API see "Network.BitTorrent.Exchange" module.
24--
25-- For more infomation see:
26-- <https://wiki.theory.org/BitTorrentSpecification#Peer_wire_protocol_.28TCP.29>
27--
28{-# LANGUAGE ViewPatterns #-}
29{-# LANGUAGE FlexibleInstances #-}
30{-# LANGUAGE FlexibleContexts #-}
31{-# LANGUAGE TypeFamilies #-}
32{-# LANGUAGE GeneralizedNewtypeDeriving #-}
33{-# LANGUAGE DeriveDataTypeable #-}
34{-# LANGUAGE TemplateHaskell #-}
35{-# OPTIONS -fno-warn-orphans #-}
36module Network.BitTorrent.Exchange.Message
37 ( -- * Capabilities
38 Capabilities (..)
39 , Extension (..)
40 , Caps
41
42 -- * Handshake
43 , ProtocolName
44 , Handshake(..)
45 , defaultHandshake
46 , handshakeSize
47 , handshakeMaxSize
48 , handshakeStats
49
50 -- * Stats
51 , ByteCount
52 , ByteStats (..)
53 , byteLength
54
55 -- * Messages
56 , Message (..)
57 , defaultKeepAliveTimeout
58 , defaultKeepAliveInterval
59 , PeerMessage (..)
60
61 -- ** Core messages
62 , StatusUpdate (..)
63 , Available (..)
64 , Transfer (..)
65 , defaultRequestQueueLength
66
67 -- ** Fast extension
68 , FastMessage (..)
69
70 -- ** Extension protocol
71 , ExtendedMessage (..)
72
73 -- *** Capabilities
74 , ExtendedExtension (..)
75 , ExtendedCaps (..)
76
77 -- *** Handshake
78 , ExtendedHandshake (..)
79 , defaultQueueLength
80 , nullExtendedHandshake
81
82 -- *** Metadata
83 , ExtendedMetadata (..)
84 , metadataPieceSize
85 , defaultMetadataFactor
86 , defaultMaxInfoDictSize
87 , isLastPiece
88 , isValidPiece
89 ) where
90
91import Control.Applicative
92import Control.Arrow ((&&&), (***))
93import Control.Monad (when)
94import Data.Attoparsec.ByteString.Char8 as BS
95import Data.BEncode as BE
96import Data.BEncode.BDict as BE
97import Data.BEncode.Internal as BE (ppBEncode, parser)
98import Data.BEncode.Types (BDict)
99import Data.Bits
100import Data.ByteString as BS
101import Data.ByteString.Char8 as BC
102import Data.ByteString.Lazy as BL
103import Data.Default
104import Data.List as L
105import Data.Map.Strict as M
106import Data.Maybe
107import Data.Monoid
108import Data.Ord
109import Data.Serialize as S
110import Data.String
111import Data.Text as T
112import Data.Typeable
113import Data.Word
114import Data.IP
115import Network
116import Network.Socket hiding (KeepAlive)
117import Text.PrettyPrint as PP hiding ((<>))
118import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
119
120import Data.Torrent hiding (Piece (..))
121import qualified Data.Torrent as P (Piece (..))
122import Network.Address
123import Network.BitTorrent.Exchange.Bitfield
124import Network.BitTorrent.Exchange.Block
125
126{-----------------------------------------------------------------------
127-- Capabilities
128-----------------------------------------------------------------------}
129
130-- |
131class Capabilities caps where
132 type Ext caps :: *
133
134 -- | Pack extensions to caps.
135 toCaps :: [Ext caps] -> caps
136
137 -- | Unpack extensions from caps.
138 fromCaps :: caps -> [Ext caps]
139
140 -- | Check if an extension is a member of the specified set.
141 allowed :: Ext caps -> caps -> Bool
142
143ppCaps :: Capabilities caps => Pretty (Ext caps) => caps -> Doc
144ppCaps = hcat . punctuate ", " . L.map pPrint . fromCaps
145
146{-----------------------------------------------------------------------
147-- Extensions
148-----------------------------------------------------------------------}
149
150-- | Enumeration of message extension protocols.
151--
152-- For more info see: <http://www.bittorrent.org/beps/bep_0004.html>
153--
154data Extension
155 = ExtDHT -- ^ BEP 5: allow to send PORT messages.
156 | ExtFast -- ^ BEP 6: allow to send FAST messages.
157 | ExtExtended -- ^ BEP 10: allow to send the extension protocol messages.
158 deriving (Show, Eq, Ord, Enum, Bounded)
159
160-- | Full extension names, suitable for logging.
161instance Pretty Extension where
162 pPrint ExtDHT = "Distributed Hash Table Protocol"
163 pPrint ExtFast = "Fast Extension"
164 pPrint ExtExtended = "Extension Protocol"
165
166-- | Extension bitmask as specified by BEP 4.
167extMask :: Extension -> Word64
168extMask ExtDHT = 0x01
169extMask ExtFast = 0x04
170extMask ExtExtended = 0x100000
171
172{-----------------------------------------------------------------------
173-- Capabilities
174-----------------------------------------------------------------------}
175
176-- | Capabilities is a set of 'Extension's usually sent in 'Handshake'
177-- messages.
178newtype Caps = Caps Word64
179 deriving (Show, Eq)
180
181-- | Render set of extensions as comma separated list.
182instance Pretty Caps where
183 pPrint = ppCaps
184 {-# INLINE pPrint #-}
185
186-- | The empty set.
187instance Default Caps where
188 def = Caps 0
189 {-# INLINE def #-}
190
191-- | Monoid under intersection. 'mempty' includes all known extensions.
192instance Monoid Caps where
193 mempty = toCaps [minBound .. maxBound]
194 {-# INLINE mempty #-}
195
196 mappend (Caps a) (Caps b) = Caps (a .&. b)
197 {-# INLINE mappend #-}
198
199-- | 'Handshake' compatible encoding.
200instance Serialize Caps where
201 put (Caps caps) = S.putWord64be caps
202 {-# INLINE put #-}
203
204 get = Caps <$> S.getWord64be
205 {-# INLINE get #-}
206
207instance Capabilities Caps where
208 type Ext Caps = Extension
209
210 allowed e (Caps caps) = (extMask e .&. caps) /= 0
211 {-# INLINE allowed #-}
212
213 toCaps = Caps . L.foldr (.|.) 0 . L.map extMask
214 fromCaps caps = L.filter (`allowed` caps) [minBound..maxBound]
215
216{-----------------------------------------------------------------------
217 Handshake
218-----------------------------------------------------------------------}
219
220maxProtocolNameSize :: Word8
221maxProtocolNameSize = maxBound
222
223-- | The protocol name is used to identify to the local peer which
224-- version of BTP the remote peer uses.
225newtype ProtocolName = ProtocolName BS.ByteString
226 deriving (Eq, Ord, Typeable)
227
228-- | In BTP/1.0 the name is 'BitTorrent protocol'. If this string is
229-- different from the local peers own protocol name, then the
230-- connection is to be dropped.
231instance Default ProtocolName where
232 def = ProtocolName "BitTorrent protocol"
233
234instance Show ProtocolName where
235 show (ProtocolName bs) = show bs
236
237instance Pretty ProtocolName where
238 pPrint (ProtocolName bs) = PP.text $ BC.unpack bs
239
240instance IsString ProtocolName where
241 fromString str
242 | L.length str <= fromIntegral maxProtocolNameSize
243 = ProtocolName (fromString str)
244 | otherwise = error $ "fromString: ProtocolName too long: " ++ str
245
246instance Serialize ProtocolName where
247 put (ProtocolName bs) = do
248 putWord8 $ fromIntegral $ BS.length bs
249 putByteString bs
250
251 get = do
252 len <- getWord8
253 bs <- getByteString $ fromIntegral len
254 return (ProtocolName bs)
255
256-- | Handshake message is used to exchange all information necessary
257-- to establish connection between peers.
258--
259data Handshake = Handshake {
260 -- | Identifier of the protocol. This is usually equal to 'def'.
261 hsProtocol :: ProtocolName
262
263 -- | Reserved bytes used to specify supported BEP's.
264 , hsReserved :: Caps
265
266 -- | Info hash of the info part of the metainfo file. that is
267 -- transmitted in tracker requests. Info hash of the initiator
268 -- handshake and response handshake should match, otherwise
269 -- initiator should break the connection.
270 --
271 , hsInfoHash :: InfoHash
272
273 -- | Peer id of the initiator. This is usually the same peer id
274 -- that is transmitted in tracker requests.
275 --
276 , hsPeerId :: PeerId
277
278 } deriving (Show, Eq)
279
280instance Serialize Handshake where
281 put Handshake {..} = do
282 put hsProtocol
283 put hsReserved
284 put hsInfoHash
285 put hsPeerId
286 get = Handshake <$> get <*> get <*> get <*> get
287
288-- | Show handshake protocol string, caps and fingerprint.
289instance Pretty Handshake where
290 pPrint Handshake {..}
291 = pPrint hsProtocol $$
292 pPrint hsReserved $$
293 pPrint (fingerprint hsPeerId)
294
295-- | Get handshake message size in bytes from the length of protocol
296-- string.
297handshakeSize :: Word8 -> Int
298handshakeSize n = 1 + fromIntegral n + 8 + 20 + 20
299
300-- | Maximum size of handshake message in bytes.
301handshakeMaxSize :: Int
302handshakeMaxSize = handshakeSize maxProtocolNameSize
303
304-- | Handshake with default protocol string and reserved bitmask.
305defaultHandshake :: InfoHash -> PeerId -> Handshake
306defaultHandshake = Handshake def def
307
308handshakeStats :: Handshake -> ByteStats
309handshakeStats (Handshake (ProtocolName bs) _ _ _)
310 = ByteStats 1 (BS.length bs + 8 + 20 + 20) 0
311
312{-----------------------------------------------------------------------
313-- Stats
314-----------------------------------------------------------------------}
315
316-- | Number of bytes.
317type ByteCount = Int
318
319-- | Summary of encoded message byte layout can be used to collect
320-- stats about message flow in both directions. This data can be
321-- retrieved using 'stats' function.
322data ByteStats = ByteStats
323 { -- | Number of bytes used to help encode 'control' and 'payload'
324 -- bytes: message size, message ID's, etc
325 overhead :: {-# UNPACK #-} !ByteCount
326
327 -- | Number of bytes used to exchange peers state\/options: piece
328 -- and block indexes, infohash, port numbers, peer ID\/IP, etc.
329 , control :: {-# UNPACK #-} !ByteCount
330
331 -- | Number of payload bytes: torrent data blocks and infodict
332 -- metadata.
333 , payload :: {-# UNPACK #-} !ByteCount
334 } deriving Show
335
336instance Pretty ByteStats where
337 pPrint s @ ByteStats {..} = fsep
338 [ PP.int overhead, "overhead"
339 , PP.int control, "control"
340 , PP.int payload, "payload"
341 , "bytes"
342 ] $+$ fsep
343 [ PP.int (byteLength s), "total bytes"
344 ]
345
346-- | Empty byte sequences.
347instance Default ByteStats where
348 def = ByteStats 0 0 0
349
350-- | Monoid under addition.
351instance Monoid ByteStats where
352 mempty = def
353 mappend a b = ByteStats
354 { overhead = overhead a + overhead b
355 , control = control a + control b
356 , payload = payload a + payload b
357 }
358
359-- | Sum of the all byte sequences.
360byteLength :: ByteStats -> Int
361byteLength ByteStats {..} = overhead + control + payload
362
363{-----------------------------------------------------------------------
364-- Regular messages
365-----------------------------------------------------------------------}
366
367-- | Messages which can be sent after handshaking. Minimal complete
368-- definition: 'envelop'.
369class PeerMessage a where
370 -- | Construct a message to be /sent/. Note that if 'ExtendedCaps'
371 -- do not contain mapping for this message the default
372 -- 'ExtendedMessageId' is used.
373 envelop :: ExtendedCaps -- ^ The /receiver/ extended capabilities;
374 -> a -- ^ An regular message;
375 -> Message -- ^ Enveloped message to sent.
376
377 -- | Find out the extension this message belong to. Can be used to
378 -- check if this message is allowed to send\/recv in current
379 -- session.
380 requires :: a -> Maybe Extension
381 requires _ = Nothing
382
383 -- | Get sizes of overhead\/control\/payload byte sequences of
384 -- binary message representation without encoding message to binary
385 -- bytestring.
386 --
387 -- This function should obey one law:
388 --
389 -- * 'byteLength' ('stats' msg) == 'BL.length' ('encode' msg)
390 --
391 stats :: a -> ByteStats
392 stats _ = ByteStats 4 0 0
393
394{-----------------------------------------------------------------------
395-- Status messages
396-----------------------------------------------------------------------}
397
398-- | Notification that the sender have updated its
399-- 'Network.BitTorrent.Exchange.Status.PeerStatus'.
400data StatusUpdate
401 -- | Notification that the sender will not upload data to the
402 -- receiver until unchoking happen.
403 = Choking !Bool
404
405 -- | Notification that the sender is interested (or not interested)
406 -- in any of the receiver's data pieces.
407 | Interested !Bool
408 deriving (Show, Eq, Ord, Typeable)
409
410instance Pretty StatusUpdate where
411 pPrint (Choking False) = "not choking"
412 pPrint (Choking True ) = "choking"
413 pPrint (Interested False) = "not interested"
414 pPrint (Interested True ) = "interested"
415
416instance PeerMessage StatusUpdate where
417 envelop _ = Status
418 {-# INLINE envelop #-}
419
420 stats _ = ByteStats 4 1 0
421 {-# INLINE stats #-}
422
423{-----------------------------------------------------------------------
424-- Available messages
425-----------------------------------------------------------------------}
426
427-- | Messages used to inform receiver which pieces of the torrent
428-- sender have.
429data Available =
430 -- | Zero-based index of a piece that has just been successfully
431 -- downloaded and verified via the hash.
432 Have ! PieceIx
433
434 -- | The bitfield message may only be sent immediately after the
435 -- handshaking sequence is complete, and before any other message
436 -- are sent. If client have no pieces then bitfield need not to be
437 -- sent.
438 | Bitfield !Bitfield
439 deriving (Show, Eq)
440
441instance Pretty Available where
442 pPrint (Have ix ) = "Have" <+> int ix
443 pPrint (Bitfield _ ) = "Bitfield"
444
445instance PeerMessage Available where
446 envelop _ = Available
447 {-# INLINE envelop #-}
448
449 stats (Have _) = ByteStats (4 + 1) 4 0
450 stats (Bitfield bf) = ByteStats (4 + 1) (q + trailing) 0
451 where
452 trailing = if r == 0 then 0 else 1
453 (q, r) = quotRem (totalCount bf) 8
454
455{-----------------------------------------------------------------------
456-- Transfer messages
457-----------------------------------------------------------------------}
458
459-- | Messages used to transfer 'Block's.
460data Transfer
461 -- | Request for a particular block. If a client is requested a
462 -- block that another peer do not have the peer might not answer
463 -- at all.
464 = Request ! BlockIx
465
466 -- | Response to a request for a block.
467 | Piece !(Block BL.ByteString)
468
469 -- | Used to cancel block requests. It is typically used during
470 -- "End Game".
471 | Cancel !BlockIx
472 deriving (Show, Eq)
473
474instance Pretty Transfer where
475 pPrint (Request ix ) = "Request" <+> pPrint ix
476 pPrint (Piece blk) = "Piece" <+> pPrint blk
477 pPrint (Cancel i ) = "Cancel" <+> pPrint i
478
479instance PeerMessage Transfer where
480 envelop _ = Transfer
481 {-# INLINE envelop #-}
482
483 stats (Request _ ) = ByteStats (4 + 1) (3 * 4) 0
484 stats (Piece p ) = ByteStats (4 + 1) (4 + 4 + blockSize p) 0
485 stats (Cancel _ ) = ByteStats (4 + 1) (3 * 4) 0
486
487-- TODO increase
488-- | Max number of pending 'Request's inflight.
489defaultRequestQueueLength :: Int
490defaultRequestQueueLength = 1
491
492{-----------------------------------------------------------------------
493-- Fast messages
494-----------------------------------------------------------------------}
495
496-- | BEP6 messages.
497data FastMessage =
498 -- | If a peer have all pieces it might send the 'HaveAll' message
499 -- instead of 'Bitfield' message. Used to save bandwidth.
500 HaveAll
501
502 -- | If a peer have no pieces it might send 'HaveNone' message
503 -- intead of 'Bitfield' message. Used to save bandwidth.
504 | HaveNone
505
506 -- | This is an advisory message meaning "you might like to
507 -- download this piece." Used to avoid excessive disk seeks and
508 -- amount of IO.
509 | SuggestPiece !PieceIx
510
511 -- | Notifies a requesting peer that its request will not be
512 -- satisfied.
513 | RejectRequest !BlockIx
514
515 -- | This is an advisory messsage meaning \"if you ask for this
516 -- piece, I'll give it to you even if you're choked.\" Used to
517 -- shorten starting phase.
518 | AllowedFast !PieceIx
519 deriving (Show, Eq)
520
521instance Pretty FastMessage where
522 pPrint (HaveAll ) = "Have all"
523 pPrint (HaveNone ) = "Have none"
524 pPrint (SuggestPiece pix) = "Suggest" <+> int pix
525 pPrint (RejectRequest bix) = "Reject" <+> pPrint bix
526 pPrint (AllowedFast pix) = "Allowed fast" <+> int pix
527
528instance PeerMessage FastMessage where
529 envelop _ = Fast
530 {-# INLINE envelop #-}
531
532 requires _ = Just ExtFast
533 {-# INLINE requires #-}
534
535 stats HaveAll = ByteStats 4 1 0
536 stats HaveNone = ByteStats 4 1 0
537 stats (SuggestPiece _) = ByteStats 5 4 0
538 stats (RejectRequest _) = ByteStats 5 12 0
539 stats (AllowedFast _) = ByteStats 5 4 0
540
541{-----------------------------------------------------------------------
542-- Extension protocol
543-----------------------------------------------------------------------}
544
545{-----------------------------------------------------------------------
546-- Extended capabilities
547-----------------------------------------------------------------------}
548
549data ExtendedExtension
550 = ExtMetadata -- ^ BEP 9: Extension for Peers to Send Metadata Files
551 deriving (Show, Eq, Ord, Enum, Bounded, Typeable)
552
553instance IsString ExtendedExtension where
554 fromString = fromMaybe (error msg) . fromKey . fromString
555 where
556 msg = "fromString: could not parse ExtendedExtension"
557
558instance Pretty ExtendedExtension where
559 pPrint ExtMetadata = "Extension for Peers to Send Metadata Files"
560
561fromKey :: BKey -> Maybe ExtendedExtension
562fromKey "ut_metadata" = Just ExtMetadata
563fromKey _ = Nothing
564{-# INLINE fromKey #-}
565
566toKey :: ExtendedExtension -> BKey
567toKey ExtMetadata = "ut_metadata"
568{-# INLINE toKey #-}
569
570type ExtendedMessageId = Word8
571
572extId :: ExtendedExtension -> ExtendedMessageId
573extId ExtMetadata = 1
574{-# INLINE extId #-}
575
576type ExtendedMap = Map ExtendedExtension ExtendedMessageId
577
578-- | The extension IDs must be stored for every peer, because every
579-- peer may have different IDs for the same extension.
580--
581newtype ExtendedCaps = ExtendedCaps { extendedCaps :: ExtendedMap }
582 deriving (Show, Eq)
583
584instance Pretty ExtendedCaps where
585 pPrint = ppCaps
586 {-# INLINE pPrint #-}
587
588-- | The empty set.
589instance Default ExtendedCaps where
590 def = ExtendedCaps M.empty
591
592-- | Monoid under intersection:
593--
594-- * The 'mempty' caps includes all known extensions;
595--
596-- * the 'mappend' operation is NOT commutative: it return message
597-- id from the first caps for the extensions existing in both caps.
598--
599instance Monoid ExtendedCaps where
600 mempty = toCaps [minBound..maxBound]
601 mappend (ExtendedCaps a) (ExtendedCaps b) =
602 ExtendedCaps (M.intersection a b)
603
604appendBDict :: BDict -> ExtendedMap -> ExtendedMap
605appendBDict (Cons key val xs) caps
606 | Just ext <- fromKey key
607 , Right eid <- fromBEncode val = M.insert ext eid (appendBDict xs caps)
608 | otherwise = appendBDict xs caps
609appendBDict Nil caps = caps
610
611-- | Handshake compatible encoding.
612instance BEncode ExtendedCaps where
613 toBEncode = BDict . BE.fromAscList . L.sortBy (comparing fst)
614 . L.map (toKey *** toBEncode) . M.toList . extendedCaps
615
616 fromBEncode (BDict bd) = pure $ ExtendedCaps $ appendBDict bd M.empty
617 fromBEncode _ = decodingError "ExtendedCaps"
618
619instance Capabilities ExtendedCaps where
620 type Ext ExtendedCaps = ExtendedExtension
621
622 toCaps = ExtendedCaps . M.fromList . L.map (id &&& extId)
623
624 fromCaps = M.keys . extendedCaps
625 {-# INLINE fromCaps #-}
626
627 allowed e (ExtendedCaps caps) = M.member e caps
628 {-# INLINE allowed #-}
629
630remoteMessageId :: ExtendedExtension -> ExtendedCaps -> ExtendedMessageId
631remoteMessageId ext = fromMaybe (extId ext) . M.lookup ext . extendedCaps
632
633{-----------------------------------------------------------------------
634-- Extended handshake
635-----------------------------------------------------------------------}
636
637-- | This message should be sent immediately after the standard
638-- bittorrent handshake to any peer that supports this extension
639-- protocol. Extended handshakes can be sent more than once, however
640-- an implementation may choose to ignore subsequent handshake
641-- messages.
642--
643data ExtendedHandshake = ExtendedHandshake
644 { -- | If this peer has an IPv4 interface, this is the compact
645 -- representation of that address.
646 ehsIPv4 :: Maybe HostAddress
647
648 -- | If this peer has an IPv6 interface, this is the compact
649 -- representation of that address.
650 , ehsIPv6 :: Maybe HostAddress6
651
652 -- | Dictionary of supported extension messages which maps names
653 -- of extensions to an extended message ID for each extension
654 -- message.
655 , ehsCaps :: ExtendedCaps
656
657 -- | Size of 'Data.Torrent.InfoDict' in bytes. This field should
658 -- be added if 'ExtMetadata' is enabled in current session /and/
659 -- peer have the torrent file.
660 , ehsMetadataSize :: Maybe Int
661
662 -- | Local TCP /listen/ port. Allows each side to learn about the
663 -- TCP port number of the other side.
664 , ehsPort :: Maybe PortNumber
665
666 -- | Request queue the number of outstanding 'Request' messages
667 -- this client supports without dropping any.
668 , ehsQueueLength :: Maybe Int
669
670 -- | Client name and version.
671 , ehsVersion :: Maybe Text
672
673 -- | IP of the remote end
674 , ehsYourIp :: Maybe IP
675 } deriving (Show, Eq, Typeable)
676
677extHandshakeId :: ExtendedMessageId
678extHandshakeId = 0
679
680-- | Default 'Request' queue size.
681defaultQueueLength :: Int
682defaultQueueLength = 1
683
684-- | All fields are empty.
685instance Default ExtendedHandshake where
686 def = ExtendedHandshake def def def def def def def def
687
688instance Monoid ExtendedHandshake where
689 mempty = def { ehsCaps = mempty }
690 mappend old new = ExtendedHandshake {
691 ehsCaps = ehsCaps old <> ehsCaps new,
692 ehsIPv4 = ehsIPv4 old `mergeOld` ehsIPv4 new,
693 ehsIPv6 = ehsIPv6 old `mergeOld` ehsIPv6 new,
694 ehsMetadataSize = ehsMetadataSize old `mergeNew` ehsMetadataSize new,
695 ehsPort = ehsPort old `mergeOld` ehsPort new,
696 ehsQueueLength = ehsQueueLength old `mergeNew` ehsQueueLength new,
697 ehsVersion = ehsVersion old `mergeOld` ehsVersion new,
698 ehsYourIp = ehsYourIp old `mergeOld` ehsYourIp new
699 }
700 where
701 mergeOld mold mnew = mold <|> mnew
702 mergeNew mold mnew = mnew <|> mold
703
704
705instance BEncode ExtendedHandshake where
706 toBEncode ExtendedHandshake {..} = toDict $
707 "ipv4" .=? (S.encode <$> ehsIPv4)
708 .: "ipv6" .=? (S.encode <$> ehsIPv6)
709 .: "m" .=! ehsCaps
710 .: "metadata_size" .=? ehsMetadataSize
711 .: "p" .=? ehsPort
712 .: "reqq" .=? ehsQueueLength
713 .: "v" .=? ehsVersion
714 .: "yourip" .=? (runPut <$> either put put <$> toEither <$> ehsYourIp)
715 .: endDict
716 where
717 toEither (IPv4 v4) = Left v4
718 toEither (IPv6 v6) = Right v6
719
720 fromBEncode = fromDict $ ExtendedHandshake
721 <$>? "ipv4"
722 <*>? "ipv6"
723 <*>! "m"
724 <*>? "metadata_size"
725 <*>? "p"
726 <*>? "reqq"
727 <*>? "v"
728 <*> (opt "yourip" >>= getYourIp)
729
730getYourIp :: Maybe BValue -> BE.Get (Maybe IP)
731getYourIp f =
732 return $ do
733 BString ip <- f
734 either (const Nothing) Just $
735 case BS.length ip of
736 4 -> IPv4 <$> S.decode ip
737 16 -> IPv6 <$> S.decode ip
738 _ -> fail ""
739
740instance Pretty ExtendedHandshake where
741 pPrint = PP.text . show
742
743-- | NOTE: Approximated 'stats'.
744instance PeerMessage ExtendedHandshake where
745 envelop c = envelop c . EHandshake
746 {-# INLINE envelop #-}
747
748 requires _ = Just ExtExtended
749 {-# INLINE requires #-}
750
751 stats _ = ByteStats (4 + 1 + 1) 100 {- is it ok? -} 0 -- FIXME
752 {-# INLINE stats #-}
753
754-- | Set default values and the specified 'ExtendedCaps'.
755nullExtendedHandshake :: ExtendedCaps -> ExtendedHandshake
756nullExtendedHandshake caps = ExtendedHandshake
757 { ehsIPv4 = Nothing
758 , ehsIPv6 = Nothing
759 , ehsCaps = caps
760 , ehsMetadataSize = Nothing
761 , ehsPort = Nothing
762 , ehsQueueLength = Just defaultQueueLength
763 , ehsVersion = Just $ T.pack $ render $ pPrint libFingerprint
764 , ehsYourIp = Nothing
765 }
766
767{-----------------------------------------------------------------------
768-- Metadata exchange extension
769-----------------------------------------------------------------------}
770
771-- | A peer MUST verify that any piece it sends passes the info-hash
772-- verification. i.e. until the peer has the entire metadata, it
773-- cannot run SHA-1 to verify that it yields the same hash as the
774-- info-hash.
775--
776data ExtendedMetadata
777 -- | This message requests the a specified metadata piece. The
778 -- response to this message, from a peer supporting the extension,
779 -- is either a 'MetadataReject' or a 'MetadataData' message.
780 = MetadataRequest PieceIx
781
782 -- | If sender requested a valid 'PieceIx' and receiver have the
783 -- corresponding piece then receiver should respond with this
784 -- message.
785 | MetadataData
786 { -- | A piece of 'Data.Torrent.InfoDict'.
787 piece :: P.Piece BS.ByteString
788
789 -- | This key has the same semantics as the 'ehsMetadataSize' in
790 -- the 'ExtendedHandshake' — it is size of the torrent info
791 -- dict.
792 , totalSize :: Int
793 }
794
795 -- | Peers that do not have the entire metadata MUST respond with
796 -- a reject message to any metadata request.
797 --
798 -- Clients MAY implement flood protection by rejecting request
799 -- messages after a certain number of them have been
800 -- served. Typically the number of pieces of metadata times a
801 -- factor.
802 | MetadataReject PieceIx
803
804 -- | Reserved. By specification we should ignore unknown metadata
805 -- messages.
806 | MetadataUnknown BValue
807 deriving (Show, Eq, Typeable)
808
809-- | Extended metadata message id used in 'msg_type_key'.
810type MetadataId = Int
811
812msg_type_key, piece_key, total_size_key :: BKey
813msg_type_key = "msg_type"
814piece_key = "piece"
815total_size_key = "total_size"
816
817-- | BEP9 compatible encoding.
818instance BEncode ExtendedMetadata where
819 toBEncode (MetadataRequest pix) = toDict $
820 msg_type_key .=! (0 :: MetadataId)
821 .: piece_key .=! pix
822 .: endDict
823 toBEncode (MetadataData (P.Piece pix _) totalSize) = toDict $
824 msg_type_key .=! (1 :: MetadataId)
825 .: piece_key .=! pix
826 .: total_size_key .=! totalSize
827 .: endDict
828 toBEncode (MetadataReject pix) = toDict $
829 msg_type_key .=! (2 :: MetadataId)
830 .: piece_key .=! pix
831 .: endDict
832 toBEncode (MetadataUnknown bval) = bval
833
834 fromBEncode bval = (`fromDict` bval) $ do
835 mid <- field $ req msg_type_key
836 case mid :: MetadataId of
837 0 -> MetadataRequest <$>! piece_key
838 1 -> metadataData <$>! piece_key <*>! total_size_key
839 2 -> MetadataReject <$>! piece_key
840 _ -> pure (MetadataUnknown bval)
841 where
842 metadataData pix s = MetadataData (P.Piece pix BS.empty) s
843
844-- | Piece data bytes are omitted.
845instance Pretty ExtendedMetadata where
846 pPrint (MetadataRequest pix ) = "Request" <+> PP.int pix
847 pPrint (MetadataData p t) = "Data" <+> pPrint p <+> PP.int t
848 pPrint (MetadataReject pix ) = "Reject" <+> PP.int pix
849 pPrint (MetadataUnknown bval ) = "Unknown" <+> ppBEncode bval
850
851-- | NOTE: Approximated 'stats'.
852instance PeerMessage ExtendedMetadata where
853 envelop c = envelop c . EMetadata (remoteMessageId ExtMetadata c)
854 {-# INLINE envelop #-}
855
856 requires _ = Just ExtExtended
857 {-# INLINE requires #-}
858
859 stats (MetadataRequest _) = ByteStats (4 + 1 + 1) {- ~ -} 25 0
860 stats (MetadataData p _) = ByteStats (4 + 1 + 1) {- ~ -} 41 $
861 BS.length (P.pieceData p)
862 stats (MetadataReject _) = ByteStats (4 + 1 + 1) {- ~ -} 25 0
863 stats (MetadataUnknown _) = ByteStats (4 + 1 + 1) {- ? -} 0 0
864
865-- | All 'Piece's in 'MetadataData' messages MUST have size equal to
866-- this value. The last trailing piece can be shorter.
867metadataPieceSize :: PieceSize
868metadataPieceSize = 16 * 1024
869
870isLastPiece :: P.Piece a -> Int -> Bool
871isLastPiece P.Piece {..} total = succ pieceIndex == pcnt
872 where
873 pcnt = q + if r > 0 then 1 else 0
874 (q, r) = quotRem total metadataPieceSize
875
876-- TODO we can check if the piece payload bytestring have appropriate
877-- length; otherwise serialization MUST fail.
878isValidPiece :: P.Piece BL.ByteString -> Int -> Bool
879isValidPiece p @ P.Piece {..} total
880 | isLastPiece p total = pieceSize p <= metadataPieceSize
881 | otherwise = pieceSize p == metadataPieceSize
882
883setMetadataPayload :: BS.ByteString -> ExtendedMetadata -> ExtendedMetadata
884setMetadataPayload bs (MetadataData (P.Piece pix _) t) =
885 MetadataData (P.Piece pix bs) t
886setMetadataPayload _ msg = msg
887
888getMetadataPayload :: ExtendedMetadata -> Maybe BS.ByteString
889getMetadataPayload (MetadataData (P.Piece _ bs) _) = Just bs
890getMetadataPayload _ = Nothing
891
892-- | Metadata BDict usually contain only 'msg_type_key', 'piece_key'
893-- and 'total_size_key' fields so it normally should take less than
894-- 100 bytes. This limit is two order of magnitude larger to be
895-- permissive to 'MetadataUnknown' messages.
896--
897-- See 'maxMessageSize' for further explanation.
898--
899maxMetadataBDictSize :: Int
900maxMetadataBDictSize = 16 * 1024
901
902maxMetadataSize :: Int
903maxMetadataSize = maxMetadataBDictSize + metadataPieceSize
904
905-- to make MetadataData constructor fields a little bit prettier we
906-- cheat here: first we read empty 'pieceData' from bdict, but then we
907-- fill that field with the actual piece data — trailing bytes of
908-- the message
909getMetadata :: Int -> S.Get ExtendedMetadata
910getMetadata len
911 | len > maxMetadataSize = fail $ parseError "size exceeded limit"
912 | otherwise = do
913 bs <- getByteString len
914 parseRes $ BS.parse BE.parser bs
915 where
916 parseError reason = "unable to parse metadata message: " ++ reason
917
918 parseRes (BS.Fail _ _ m) = fail $ parseError $ "bdict: " ++ m
919 parseRes (BS.Partial _) = fail $ parseError "bdict: not enough bytes"
920 parseRes (BS.Done piece bvalueBS)
921 | BS.length piece > metadataPieceSize
922 = fail "infodict piece: size exceeded limit"
923 | otherwise = do
924 metadata <- either (fail . parseError) pure $ fromBEncode bvalueBS
925 return $ setMetadataPayload piece metadata
926
927putMetadata :: ExtendedMetadata -> BL.ByteString
928putMetadata msg
929 | Just bs <- getMetadataPayload msg = BE.encode msg <> BL.fromStrict bs
930 | otherwise = BE.encode msg
931
932-- | Allows a requesting peer to send 2 'MetadataRequest's for the
933-- each piece.
934--
935-- See 'Network.BitTorrent.Wire.Options.metadataFactor' for
936-- explanation why do we need this limit.
937defaultMetadataFactor :: Int
938defaultMetadataFactor = 2
939
940-- | Usually torrent size do not exceed 1MB. This value limit torrent
941-- /content/ size to about 8TB.
942--
943-- See 'Network.BitTorrent.Wire.Options.maxInfoDictSize' for
944-- explanation why do we need this limit.
945defaultMaxInfoDictSize :: Int
946defaultMaxInfoDictSize = 10 * 1024 * 1024
947
948{-----------------------------------------------------------------------
949-- Extension protocol messages
950-----------------------------------------------------------------------}
951
952-- | For more info see <http://www.bittorrent.org/beps/bep_0010.html>
953data ExtendedMessage
954 = EHandshake ExtendedHandshake
955 | EMetadata ExtendedMessageId ExtendedMetadata
956 | EUnknown ExtendedMessageId BS.ByteString
957 deriving (Show, Eq, Typeable)
958
959instance Pretty ExtendedMessage where
960 pPrint (EHandshake ehs) = pPrint ehs
961 pPrint (EMetadata _ msg) = "Metadata" <+> pPrint msg
962 pPrint (EUnknown mid _ ) = "Unknown" <+> PP.text (show mid)
963
964instance PeerMessage ExtendedMessage where
965 envelop _ = Extended
966 {-# INLINE envelop #-}
967
968 requires _ = Just ExtExtended
969 {-# INLINE requires #-}
970
971 stats (EHandshake hs) = stats hs
972 stats (EMetadata _ msg) = stats msg
973 stats (EUnknown _ msg) = ByteStats (4 + 1 + 1) (BS.length msg) 0
974
975{-----------------------------------------------------------------------
976-- The message datatype
977-----------------------------------------------------------------------}
978
979type MessageId = Word8
980
981-- | Messages used in communication between peers.
982--
983-- Note: If some extensions are disabled (not present in extension
984-- mask) and client receive message used by the disabled
985-- extension then the client MUST close the connection.
986--
987data Message
988 -- | Peers may close the TCP connection if they have not received
989 -- any messages for a given period of time, generally 2
990 -- minutes. Thus, the KeepAlive message is sent to keep the
991 -- connection between two peers alive, if no /other/ message has
992 -- been sent in a given period of time.
993 = KeepAlive
994 | Status !StatusUpdate -- ^ Messages used to update peer status.
995 | Available !Available -- ^ Messages used to inform availability.
996 | Transfer !Transfer -- ^ Messages used to transfer 'Block's.
997
998 -- | Peer receiving a handshake indicating the remote peer
999 -- supports the 'ExtDHT' should send a 'Port' message. Peers that
1000 -- receive this message should attempt to ping the node on the
1001 -- received port and IP address of the remote peer.
1002 | Port !PortNumber
1003 | Fast !FastMessage
1004 | Extended !ExtendedMessage
1005 deriving (Show, Eq)
1006
1007instance Default Message where
1008 def = KeepAlive
1009 {-# INLINE def #-}
1010
1011-- | Payload bytes are omitted.
1012instance Pretty Message where
1013 pPrint (KeepAlive ) = "Keep alive"
1014 pPrint (Status m) = "Status" <+> pPrint m
1015 pPrint (Available m) = pPrint m
1016 pPrint (Transfer m) = pPrint m
1017 pPrint (Port p) = "Port" <+> int (fromEnum p)
1018 pPrint (Fast m) = pPrint m
1019 pPrint (Extended m) = pPrint m
1020
1021instance PeerMessage Message where
1022 envelop _ = id
1023 {-# INLINE envelop #-}
1024
1025 requires KeepAlive = Nothing
1026 requires (Status _) = Nothing
1027 requires (Available _) = Nothing
1028 requires (Transfer _) = Nothing
1029 requires (Port _) = Just ExtDHT
1030 requires (Fast _) = Just ExtFast
1031 requires (Extended _) = Just ExtExtended
1032
1033 stats KeepAlive = ByteStats 4 0 0
1034 stats (Status m) = stats m
1035 stats (Available m) = stats m
1036 stats (Transfer m) = stats m
1037 stats (Port _) = ByteStats 5 2 0
1038 stats (Fast m) = stats m
1039 stats (Extended m) = stats m
1040
1041-- | PORT message.
1042instance PeerMessage PortNumber where
1043 envelop _ = Port
1044 {-# INLINE envelop #-}
1045
1046 requires _ = Just ExtDHT
1047 {-# INLINE requires #-}
1048
1049-- | How long /this/ peer should wait before dropping connection, in
1050-- seconds.
1051defaultKeepAliveTimeout :: Int
1052defaultKeepAliveTimeout = 2 * 60
1053
1054-- | How often /this/ peer should send 'KeepAlive' messages, in
1055-- seconds.
1056defaultKeepAliveInterval :: Int
1057defaultKeepAliveInterval = 60
1058
1059getInt :: S.Get Int
1060getInt = fromIntegral <$> S.getWord32be
1061{-# INLINE getInt #-}
1062
1063putInt :: S.Putter Int
1064putInt = S.putWord32be . fromIntegral
1065{-# INLINE putInt #-}
1066
1067-- | This limit should protect against "out-of-memory" attacks: if a
1068-- malicious peer have sent a long varlength message then receiver can
1069-- accumulate too long bytestring in the 'Get'.
1070--
1071-- Normal messages should never exceed this limits.
1072--
1073-- See also 'maxBitfieldSize', 'maxBlockSize' limits.
1074--
1075maxMessageSize :: Int
1076maxMessageSize = 20 + 1024 * 1024
1077
1078-- | This also limit max torrent size to:
1079--
1080-- max_bitfield_size * piece_ix_per_byte * max_piece_size =
1081-- 2 ^ 20 * 8 * 1MB =
1082-- 8TB
1083--
1084maxBitfieldSize :: Int
1085maxBitfieldSize = 1024 * 1024
1086
1087getBitfield :: Int -> S.Get Bitfield
1088getBitfield len
1089 | len > maxBitfieldSize = fail "BITFIELD message size exceeded limit"
1090 | otherwise = fromBitmap <$> getByteString len
1091
1092maxBlockSize :: Int
1093maxBlockSize = 4 * defaultTransferSize
1094
1095getBlock :: Int -> S.Get (Block BL.ByteString)
1096getBlock len
1097 | len > maxBlockSize = fail "BLOCK message size exceeded limit"
1098 | otherwise = Block <$> getInt <*> getInt
1099 <*> getLazyByteString (fromIntegral len)
1100{-# INLINE getBlock #-}
1101
1102instance Serialize Message where
1103 get = do
1104 len <- getInt
1105
1106 when (len > maxMessageSize) $ do
1107 fail "message body size exceeded the limit"
1108
1109 if len == 0 then return KeepAlive
1110 else do
1111 mid <- S.getWord8
1112 case mid of
1113 0x00 -> return $ Status (Choking True)
1114 0x01 -> return $ Status (Choking False)
1115 0x02 -> return $ Status (Interested True)
1116 0x03 -> return $ Status (Interested False)
1117 0x04 -> (Available . Have) <$> getInt
1118 0x05 -> (Available . Bitfield) <$> getBitfield (pred len)
1119 0x06 -> (Transfer . Request) <$> S.get
1120 0x07 -> (Transfer . Piece) <$> getBlock (len - 9)
1121 0x08 -> (Transfer . Cancel) <$> S.get
1122 0x09 -> Port <$> S.get
1123 0x0D -> (Fast . SuggestPiece) <$> getInt
1124 0x0E -> return $ Fast HaveAll
1125 0x0F -> return $ Fast HaveNone
1126 0x10 -> (Fast . RejectRequest) <$> S.get
1127 0x11 -> (Fast . AllowedFast) <$> getInt
1128 0x14 -> Extended <$> getExtendedMessage (pred len)
1129 _ -> do
1130 rm <- S.remaining >>= S.getBytes
1131 fail $ "unknown message ID: " ++ show mid ++ "\n"
1132 ++ "remaining available bytes: " ++ show rm
1133
1134 put KeepAlive = putInt 0
1135 put (Status msg) = putStatus msg
1136 put (Available msg) = putAvailable msg
1137 put (Transfer msg) = putTransfer msg
1138 put (Port p ) = putPort p
1139 put (Fast msg) = putFast msg
1140 put (Extended m ) = putExtendedMessage m
1141
1142statusUpdateId :: StatusUpdate -> MessageId
1143statusUpdateId (Choking choking) = fromIntegral (0 + fromEnum choking)
1144statusUpdateId (Interested choking) = fromIntegral (2 + fromEnum choking)
1145
1146putStatus :: Putter StatusUpdate
1147putStatus su = do
1148 putInt 1
1149 putWord8 (statusUpdateId su)
1150
1151putAvailable :: Putter Available
1152putAvailable (Have i) = do
1153 putInt 5
1154 putWord8 0x04
1155 putInt i
1156putAvailable (Bitfield (toBitmap -> bs)) = do
1157 putInt $ 1 + fromIntegral (BL.length bs)
1158 putWord8 0x05
1159 putLazyByteString bs
1160
1161putBlock :: Putter (Block BL.ByteString)
1162putBlock Block {..} = do
1163 putInt blkPiece
1164 putInt blkOffset
1165 putLazyByteString blkData
1166
1167putTransfer :: Putter Transfer
1168putTransfer (Request blk) = putInt 13 >> S.putWord8 0x06 >> S.put blk
1169putTransfer (Piece blk) = do
1170 putInt (9 + blockSize blk)
1171 putWord8 0x07
1172 putBlock blk
1173putTransfer (Cancel blk) = putInt 13 >> S.putWord8 0x08 >> S.put blk
1174
1175putPort :: Putter PortNumber
1176putPort p = do
1177 putInt 3
1178 putWord8 0x09
1179 put p
1180
1181putFast :: Putter FastMessage
1182putFast HaveAll = putInt 1 >> putWord8 0x0E
1183putFast HaveNone = putInt 1 >> putWord8 0x0F
1184putFast (SuggestPiece pix) = putInt 5 >> putWord8 0x0D >> putInt pix
1185putFast (RejectRequest i ) = putInt 13 >> putWord8 0x10 >> put i
1186putFast (AllowedFast i ) = putInt 5 >> putWord8 0x11 >> putInt i
1187
1188maxEHandshakeSize :: Int
1189maxEHandshakeSize = 16 * 1024
1190
1191getExtendedHandshake :: Int -> S.Get ExtendedHandshake
1192getExtendedHandshake messageSize
1193 | messageSize > maxEHandshakeSize
1194 = fail "extended handshake size exceeded limit"
1195 | otherwise = do
1196 bs <- getByteString messageSize
1197 either fail pure $ BE.decode bs
1198
1199maxEUnknownSize :: Int
1200maxEUnknownSize = 64 * 1024
1201
1202getExtendedUnknown :: Int -> S.Get BS.ByteString
1203getExtendedUnknown len
1204 | len > maxEUnknownSize = fail "unknown extended message size exceeded limit"
1205 | otherwise = getByteString len
1206
1207getExtendedMessage :: Int -> S.Get ExtendedMessage
1208getExtendedMessage messageSize = do
1209 msgId <- getWord8
1210 let msgBodySize = messageSize - 1
1211 case msgId of
1212 0 -> EHandshake <$> getExtendedHandshake msgBodySize
1213 1 -> EMetadata msgId <$> getMetadata msgBodySize
1214 _ -> EUnknown msgId <$> getExtendedUnknown msgBodySize
1215
1216-- | By spec.
1217extendedMessageId :: MessageId
1218extendedMessageId = 20
1219
1220putExt :: ExtendedMessageId -> BL.ByteString -> Put
1221putExt mid lbs = do
1222 putWord32be $ fromIntegral (1 + 1 + BL.length lbs)
1223 putWord8 extendedMessageId
1224 putWord8 mid
1225 putLazyByteString lbs
1226
1227-- NOTE: in contrast to getExtendedMessage this function put length
1228-- and message id too!
1229putExtendedMessage :: Putter ExtendedMessage
1230putExtendedMessage (EHandshake hs) = putExt extHandshakeId $ BE.encode hs
1231putExtendedMessage (EMetadata mid msg) = putExt mid $ putMetadata msg
1232putExtendedMessage (EUnknown mid bs) = putExt mid $ BL.fromStrict bs
diff --git a/bittorrent/src/Network/BitTorrent/Exchange/Session.hs b/bittorrent/src/Network/BitTorrent/Exchange/Session.hs
new file mode 100644
index 00000000..38a3c3a6
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Exchange/Session.hs
@@ -0,0 +1,586 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE StandaloneDeriving #-}
5{-# LANGUAGE TemplateHaskell #-}
6{-# LANGUAGE TypeFamilies #-}
7module Network.BitTorrent.Exchange.Session
8 ( -- * Session
9 Session
10 , Event (..)
11 , LogFun
12 , sessionLogger
13
14 -- * Construction
15 , newSession
16 , closeSession
17 , withSession
18
19 -- * Connection Set
20 , connect
21 , connectSink
22 , establish
23
24 -- * Query
25 , waitMetadata
26 , takeMetadata
27 ) where
28
29import Control.Applicative
30import Control.Concurrent
31import Control.Concurrent.Chan.Split as CS
32import Control.Concurrent.STM
33import Control.Exception hiding (Handler)
34import Control.Lens
35import Control.Monad as M
36import Control.Monad.Logger
37import Control.Monad.Reader
38import Data.ByteString as BS
39import Data.ByteString.Lazy as BL
40import Data.Conduit as C (Sink, awaitForever, (=$=), ($=))
41import qualified Data.Conduit as C
42import Data.Conduit.List as C
43import Data.Map as M
44import Data.Monoid
45import Data.Set as S
46import Data.Text as T
47import Data.Typeable
48import Text.PrettyPrint hiding ((<>))
49import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
50import System.Log.FastLogger (LogStr, ToLogStr (..))
51
52import Data.BEncode as BE
53import Data.Torrent as Torrent
54import Network.BitTorrent.Internal.Types
55import Network.Address
56import Network.BitTorrent.Exchange.Bitfield as BF
57import Network.BitTorrent.Exchange.Block as Block
58import Network.BitTorrent.Exchange.Connection
59import Network.BitTorrent.Exchange.Download as D
60import Network.BitTorrent.Exchange.Message as Message
61import System.Torrent.Storage
62
63#if !MIN_VERSION_iproute(1,2,12)
64deriving instance Ord IP
65#endif
66
67{-----------------------------------------------------------------------
68-- Exceptions
69-----------------------------------------------------------------------}
70
71data ExchangeError
72 = InvalidRequest BlockIx StorageFailure
73 | CorruptedPiece PieceIx
74 deriving (Show, Typeable)
75
76instance Exception ExchangeError
77
78packException :: Exception e => (e -> ExchangeError) -> IO a -> IO a
79packException f m = try m >>= either (throwIO . f) return
80
81{-----------------------------------------------------------------------
82-- Session state
83-----------------------------------------------------------------------}
84-- TODO unmap storage on zero connections
85
86data Cached a = Cached
87 { cachedValue :: !a
88 , cachedData :: BL.ByteString -- keep lazy
89 }
90
91cache :: BEncode a => a -> Cached a
92cache s = Cached s (BE.encode s)
93
94-- | Logger function.
95type LogFun = Loc -> LogSource -> LogLevel -> LogStr -> IO ()
96
97--data SessionStatus = Seeder | Leecher
98
99data SessionState
100 = WaitingMetadata
101 { metadataDownload :: MVar MetadataDownload
102 , metadataCompleted :: MVar InfoDict -- ^ used to unblock waiters
103 , contentRootPath :: FilePath
104 }
105 | HavingMetadata
106 { metadataCache :: Cached InfoDict
107 , contentDownload :: MVar ContentDownload
108 , contentStorage :: Storage
109 }
110
111newSessionState :: FilePath -> Either InfoHash InfoDict -> IO SessionState
112newSessionState rootPath (Left ih ) = do
113 WaitingMetadata <$> newMVar def <*> newEmptyMVar <*> pure rootPath
114newSessionState rootPath (Right dict) = do
115 storage <- openInfoDict ReadWriteEx rootPath dict
116 download <- newMVar $ D.contentDownload (BF.haveNone (totalPieces storage))
117 (piPieceLength (idPieceInfo dict))
118 storage
119 return $ HavingMetadata (cache dict) download storage
120
121closeSessionState :: SessionState -> IO ()
122closeSessionState WaitingMetadata {..} = return ()
123closeSessionState HavingMetadata {..} = close contentStorage
124
125haveMetadata :: InfoDict -> SessionState -> IO SessionState
126haveMetadata dict WaitingMetadata {..} = do
127 storage <- openInfoDict ReadWriteEx contentRootPath dict
128 download <- newMVar $ D.contentDownload (BF.haveNone (totalPieces storage))
129 (piPieceLength (idPieceInfo dict))
130 storage
131 return HavingMetadata
132 { metadataCache = cache dict
133 , contentDownload = download
134 , contentStorage = storage
135 }
136haveMetadata _ s = return s
137
138{-----------------------------------------------------------------------
139-- Session
140-----------------------------------------------------------------------}
141
142data Session = Session
143 { sessionPeerId :: !(PeerId)
144 , sessionTopic :: !(InfoHash)
145 , sessionLogger :: !(LogFun)
146 , sessionEvents :: !(SendPort (Event Session))
147
148 , sessionState :: !(MVar SessionState)
149
150------------------------------------------------------------------------
151 , connectionsPrefs :: !ConnectionPrefs
152
153 -- | Connections either waiting for TCP/uTP 'connect' or waiting
154 -- for BT handshake.
155 , connectionsPending :: !(TVar (Set (PeerAddr IP)))
156
157 -- | Connections successfully handshaked and data transfer can
158 -- take place.
159 , connectionsEstablished :: !(TVar (Map (PeerAddr IP) (Connection Session)))
160
161 -- | TODO implement choking mechanism
162 , connectionsUnchoked :: [PeerAddr IP]
163
164 -- | Messages written to this channel will be sent to the all
165 -- connections, including pending connections (but right after
166 -- handshake).
167 , connectionsBroadcast :: !(Chan Message)
168 }
169
170instance EventSource Session where
171 data Event Session
172 = ConnectingTo (PeerAddr IP)
173 | ConnectionEstablished (PeerAddr IP)
174 | ConnectionAborted
175 | ConnectionClosed (PeerAddr IP)
176 | SessionClosed
177 deriving Show
178
179 listen Session {..} = CS.listen sessionEvents
180
181newSession :: LogFun
182 -> PeerAddr (Maybe IP) -- ^ /external/ address of this peer;
183 -> FilePath -- ^ root directory for content files;
184 -> Either InfoHash InfoDict -- ^ torrent info dictionary;
185 -> IO Session
186newSession logFun addr rootPath source = do
187 let ih = either id idInfoHash source
188 pid <- maybe genPeerId return (peerId addr)
189 eventStream <- newSendPort
190 sState <- newSessionState rootPath source
191 sStateVar <- newMVar sState
192 pSetVar <- newTVarIO S.empty
193 eSetVar <- newTVarIO M.empty
194 chan <- newChan
195 return Session
196 { sessionPeerId = pid
197 , sessionTopic = ih
198 , sessionLogger = logFun
199 , sessionEvents = eventStream
200 , sessionState = sStateVar
201 , connectionsPrefs = def
202 , connectionsPending = pSetVar
203 , connectionsEstablished = eSetVar
204 , connectionsUnchoked = []
205 , connectionsBroadcast = chan
206 }
207
208closeSession :: Session -> IO ()
209closeSession Session {..} = do
210 s <- readMVar sessionState
211 closeSessionState s
212{-
213 hSet <- atomically $ do
214 pSet <- swapTVar connectionsPending S.empty
215 eSet <- swapTVar connectionsEstablished S.empty
216 return pSet
217 mapM_ kill hSet
218-}
219
220withSession :: ()
221withSession = error "withSession"
222
223{-----------------------------------------------------------------------
224-- Logging
225-----------------------------------------------------------------------}
226
227instance MonadLogger (Connected Session) where
228 monadLoggerLog loc src lvl msg = do
229 conn <- ask
230 ses <- asks connSession
231 addr <- asks connRemoteAddr
232 let addrSrc = src <> " @ " <> T.pack (render (pPrint addr))
233 liftIO $ sessionLogger ses loc addrSrc lvl (toLogStr msg)
234
235logMessage :: MonadLogger m => Message -> m ()
236logMessage msg = logDebugN $ T.pack (render (pPrint msg))
237
238logEvent :: MonadLogger m => Text -> m ()
239logEvent = logInfoN
240
241{-----------------------------------------------------------------------
242-- Connection set
243-----------------------------------------------------------------------}
244--- Connection status transition:
245---
246--- pending -> established -> finished -> closed
247--- | \|/ /|\
248--- \-------------------------------------|
249---
250--- Purpose of slots:
251--- 1) to avoid duplicates
252--- 2) connect concurrently
253---
254
255-- | Add connection to the pending set.
256pendingConnection :: PeerAddr IP -> Session -> STM Bool
257pendingConnection addr Session {..} = do
258 pSet <- readTVar connectionsPending
259 eSet <- readTVar connectionsEstablished
260 if (addr `S.member` pSet) || (addr `M.member` eSet)
261 then return False
262 else do
263 modifyTVar' connectionsPending (S.insert addr)
264 return True
265
266-- | Pending connection successfully established, add it to the
267-- established set.
268establishedConnection :: Connected Session ()
269establishedConnection = do
270 conn <- ask
271 addr <- asks connRemoteAddr
272 Session {..} <- asks connSession
273 liftIO $ atomically $ do
274 modifyTVar connectionsPending (S.delete addr)
275 modifyTVar connectionsEstablished (M.insert addr conn)
276
277-- | Either this or remote peer decided to finish conversation
278-- (conversation is alread /established/ connection), remote it from
279-- the established set.
280finishedConnection :: Connected Session ()
281finishedConnection = do
282 Session {..} <- asks connSession
283 addr <- asks connRemoteAddr
284 liftIO $ atomically $ do
285 modifyTVar connectionsEstablished $ M.delete addr
286
287-- | There are no state for this connection, remove it from the all
288-- sets.
289closedConnection :: PeerAddr IP -> Session -> STM ()
290closedConnection addr Session {..} = do
291 modifyTVar connectionsPending $ S.delete addr
292 modifyTVar connectionsEstablished $ M.delete addr
293
294getConnectionConfig :: Session -> IO (ConnectionConfig Session)
295getConnectionConfig s @ Session {..} = do
296 chan <- dupChan connectionsBroadcast
297 let sessionLink = SessionLink {
298 linkTopic = sessionTopic
299 , linkPeerId = sessionPeerId
300 , linkMetadataSize = Nothing
301 , linkOutputChan = Just chan
302 , linkSession = s
303 }
304 return ConnectionConfig
305 { cfgPrefs = connectionsPrefs
306 , cfgSession = sessionLink
307 , cfgWire = mainWire
308 }
309
310type Finalizer = IO ()
311type Runner = (ConnectionConfig Session -> IO ())
312
313runConnection :: Runner -> Finalizer -> PeerAddr IP -> Session -> IO ()
314runConnection runner finalize addr set @ Session {..} = do
315 _ <- forkIO (action `finally` cleanup)
316 return ()
317 where
318 action = do
319 notExist <- atomically $ pendingConnection addr set
320 when notExist $ do
321 cfg <- getConnectionConfig set
322 runner cfg
323
324 cleanup = do
325 finalize
326-- runStatusUpdates status (SS.resetPending addr)
327 -- TODO Metata.resetPending addr
328 atomically $ closedConnection addr set
329
330-- | Establish connection from scratch. If this endpoint is already
331-- connected, no new connections is created. This function do not block.
332connect :: PeerAddr IP -> Session -> IO ()
333connect addr = runConnection (connectWire addr) (return ()) addr
334
335-- | Establish connection with already pre-connected endpoint. If this
336-- endpoint is already connected, no new connections is created. This
337-- function do not block.
338--
339-- 'PendingConnection' will be closed automatically, you do not need
340-- to call 'closePending'.
341establish :: PendingConnection -> Session -> IO ()
342establish conn = runConnection (acceptWire conn) (closePending conn)
343 (pendingPeer conn)
344
345-- | Conduit version of 'connect'.
346connectSink :: MonadIO m => Session -> Sink [PeerAddr IPv4] m ()
347connectSink s = C.mapM_ (liftIO . connectBatch)
348 where
349 connectBatch = M.mapM_ (\ addr -> connect (IPv4 <$> addr) s)
350
351-- | Why do we need this message?
352type BroadcastMessage = ExtendedCaps -> Message
353
354broadcast :: BroadcastMessage -> Session -> IO ()
355broadcast = error "broadcast"
356
357{-----------------------------------------------------------------------
358-- Helpers
359-----------------------------------------------------------------------}
360
361waitMVar :: MVar a -> IO ()
362waitMVar m = withMVar m (const (return ()))
363
364-- This function appear in new GHC "out of box". (moreover it is atomic)
365tryReadMVar :: MVar a -> IO (Maybe a)
366tryReadMVar m = do
367 ma <- tryTakeMVar m
368 maybe (return ()) (putMVar m) ma
369 return ma
370
371readBlock :: BlockIx -> Storage -> IO (Block BL.ByteString)
372readBlock bix @ BlockIx {..} s = do
373 p <- packException (InvalidRequest bix) $ do readPiece ixPiece s
374 let chunk = BL.take (fromIntegral ixLength) $
375 BL.drop (fromIntegral ixOffset) (pieceData p)
376 if BL.length chunk == fromIntegral ixLength
377 then return $ Block ixPiece ixOffset chunk
378 else throwIO $ InvalidRequest bix (InvalidSize ixLength)
379
380-- |
381tryReadMetadataBlock :: PieceIx
382 -> Connected Session (Maybe (Torrent.Piece BS.ByteString, Int))
383tryReadMetadataBlock pix = do
384 Session {..} <- asks connSession
385 s <- liftIO (readMVar sessionState)
386 case s of
387 WaitingMetadata {..} -> error "tryReadMetadataBlock"
388 HavingMetadata {..} -> error "tryReadMetadataBlock"
389
390sendBroadcast :: PeerMessage msg => msg -> Wire Session ()
391sendBroadcast msg = do
392 Session {..} <- asks connSession
393 error "sendBroadcast"
394-- liftIO $ msg `broadcast` sessionConnections
395
396waitMetadata :: Session -> IO InfoDict
397waitMetadata Session {..} = do
398 s <- readMVar sessionState
399 case s of
400 WaitingMetadata {..} -> readMVar metadataCompleted
401 HavingMetadata {..} -> return (cachedValue metadataCache)
402
403takeMetadata :: Session -> IO (Maybe InfoDict)
404takeMetadata Session {..} = do
405 s <- readMVar sessionState
406 case s of
407 WaitingMetadata {..} -> return Nothing
408 HavingMetadata {..} -> return (Just (cachedValue metadataCache))
409
410{-----------------------------------------------------------------------
411-- Triggers
412-----------------------------------------------------------------------}
413
414-- | Trigger is the reaction of a handler at some event.
415type Trigger = Wire Session ()
416
417interesting :: Trigger
418interesting = do
419 addr <- asks connRemoteAddr
420 sendMessage (Interested True)
421 sendMessage (Choking False)
422 tryFillRequestQueue
423
424fillRequestQueue :: Trigger
425fillRequestQueue = do
426 maxN <- lift getMaxQueueLength
427 rbf <- use connBitfield
428 addr <- asks connRemoteAddr
429-- blks <- withStatusUpdates $ do
430-- n <- getRequestQueueLength addr
431-- scheduleBlocks addr rbf (maxN - n)
432-- mapM_ (sendMessage . Request) blks
433 return ()
434
435tryFillRequestQueue :: Trigger
436tryFillRequestQueue = do
437 allowed <- canDownload <$> use connStatus
438 when allowed $ do
439 fillRequestQueue
440
441{-----------------------------------------------------------------------
442-- Incoming message handling
443-----------------------------------------------------------------------}
444
445type Handler msg = msg -> Wire Session ()
446
447handleStatus :: Handler StatusUpdate
448handleStatus s = do
449 connStatus %= over remoteStatus (updateStatus s)
450 case s of
451 Interested _ -> return ()
452 Choking True -> do
453 addr <- asks connRemoteAddr
454-- withStatusUpdates (SS.resetPending addr)
455 return ()
456 Choking False -> tryFillRequestQueue
457
458handleAvailable :: Handler Available
459handleAvailable msg = do
460 connBitfield %= case msg of
461 Have ix -> BF.insert ix
462 Bitfield bf -> const bf
463
464 --thisBf <- getThisBitfield
465 thisBf <- undefined
466 case msg of
467 Have ix
468 | ix `BF.member` thisBf -> return ()
469 | otherwise -> interesting
470 Bitfield bf
471 | bf `BF.isSubsetOf` thisBf -> return ()
472 | otherwise -> interesting
473
474handleTransfer :: Handler Transfer
475handleTransfer (Request bix) = do
476 Session {..} <- asks connSession
477 s <- liftIO $ readMVar sessionState
478 case s of
479 WaitingMetadata {..} -> return ()
480 HavingMetadata {..} -> do
481 bitfield <- undefined -- getThisBitfield
482 upload <- canUpload <$> use connStatus
483 when (upload && ixPiece bix `BF.member` bitfield) $ do
484 blk <- liftIO $ readBlock bix contentStorage
485 sendMessage (Message.Piece blk)
486
487handleTransfer (Message.Piece blk) = do
488 Session {..} <- asks connSession
489 s <- liftIO $ readMVar sessionState
490 case s of
491 WaitingMetadata {..} -> return () -- TODO (?) break connection
492 HavingMetadata {..} -> do
493 isSuccess <- undefined -- withStatusUpdates (SS.pushBlock blk storage)
494 case isSuccess of
495 Nothing -> liftIO $ throwIO $ userError "block is not requested"
496 Just isCompleted -> do
497 when isCompleted $ do
498 sendBroadcast (Have (blkPiece blk))
499-- maybe send not interested
500 tryFillRequestQueue
501
502handleTransfer (Cancel bix) = filterQueue (not . (transferResponse bix))
503 where
504 transferResponse bix (Transfer (Message.Piece blk)) = blockIx blk == bix
505 transferResponse _ _ = False
506
507{-----------------------------------------------------------------------
508-- Metadata exchange
509-----------------------------------------------------------------------}
510-- TODO introduce new metadata exchange specific exceptions
511
512waitForMetadata :: Trigger
513waitForMetadata = do
514 Session {..} <- asks connSession
515 needFetch <- undefined --liftIO (isEmptyMVar infodict)
516 when needFetch $ do
517 canFetch <- allowed ExtMetadata <$> use connExtCaps
518 if canFetch
519 then tryRequestMetadataBlock
520 else undefined -- liftIO (waitMVar infodict)
521
522tryRequestMetadataBlock :: Trigger
523tryRequestMetadataBlock = do
524 mpix <- lift $ undefined --withMetadataUpdates Metadata.scheduleBlock
525 case mpix of
526 Nothing -> error "tryRequestMetadataBlock"
527 Just pix -> sendMessage (MetadataRequest pix)
528
529handleMetadata :: Handler ExtendedMetadata
530handleMetadata (MetadataRequest pix) =
531 lift (tryReadMetadataBlock pix) >>= sendMessage . mkResponse
532 where
533 mkResponse Nothing = MetadataReject pix
534 mkResponse (Just (piece, total)) = MetadataData piece total
535
536handleMetadata (MetadataData {..}) = do
537 ih <- asks connTopic
538 mdict <- lift $ undefined --withMetadataUpdates (Metadata.pushBlock piece ih)
539 case mdict of
540 Nothing -> tryRequestMetadataBlock -- not completed, need all blocks
541 Just dict -> do -- complete, wake up payload fetch
542 Session {..} <- asks connSession
543 liftIO $ modifyMVar_ sessionState (haveMetadata dict)
544
545handleMetadata (MetadataReject pix) = do
546 lift $ undefined -- withMetadataUpdates (Metadata.cancelPending pix)
547
548handleMetadata (MetadataUnknown _ ) = do
549 logInfoN "Unknown metadata message"
550
551{-----------------------------------------------------------------------
552-- Main entry point
553-----------------------------------------------------------------------}
554
555acceptRehandshake :: ExtendedHandshake -> Trigger
556acceptRehandshake ehs = error "acceptRehandshake"
557
558handleExtended :: Handler ExtendedMessage
559handleExtended (EHandshake ehs) = acceptRehandshake ehs
560handleExtended (EMetadata _ msg) = handleMetadata msg
561handleExtended (EUnknown _ _ ) = logWarnN "Unknown extension message"
562
563handleMessage :: Handler Message
564handleMessage KeepAlive = return ()
565handleMessage (Status s) = handleStatus s
566handleMessage (Available msg) = handleAvailable msg
567handleMessage (Transfer msg) = handleTransfer msg
568handleMessage (Port n) = error "handleMessage"
569handleMessage (Fast _) = error "handleMessage"
570handleMessage (Extended msg) = handleExtended msg
571
572exchange :: Wire Session ()
573exchange = do
574 waitForMetadata
575 bf <- undefined --getThisBitfield
576 sendMessage (Bitfield bf)
577 awaitForever handleMessage
578
579mainWire :: Wire Session ()
580mainWire = do
581 lift establishedConnection
582 Session {..} <- asks connSession
583-- lift $ resizeBitfield (totalPieces storage)
584 logEvent "Connection established"
585 iterM logMessage =$= exchange =$= iterM logMessage
586 lift finishedConnection
diff --git a/bittorrent/src/Network/BitTorrent/Internal/Cache.hs b/bittorrent/src/Network/BitTorrent/Internal/Cache.hs
new file mode 100644
index 00000000..8c74467a
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Internal/Cache.hs
@@ -0,0 +1,169 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Cached data for tracker responses.
9--
10module Network.BitTorrent.Internal.Cache
11 ( -- * Cache
12 Cached
13 , lastUpdated
14 , updateInterval
15 , minUpdateInterval
16
17 -- * Construction
18 , newCached
19 , newCached_
20
21 -- * Query
22 , isAlive
23 , isStalled
24 , isExpired
25 , canUpdate
26 , shouldUpdate
27
28 -- * Cached data
29 , tryTakeData
30 , unsafeTryTakeData
31 , takeData
32 ) where
33
34import Control.Applicative
35import Data.Monoid
36import Data.Default
37import Data.Time
38import Data.Time.Clock.POSIX
39import System.IO.Unsafe
40
41
42data Cached a = Cached
43 { -- | Time of resource creation.
44 lastUpdated :: !POSIXTime
45
46 -- | Minimum invalidation timeout.
47 , minUpdateInterval :: !NominalDiffTime
48
49 -- | Resource lifetime.
50 , updateInterval :: !NominalDiffTime
51
52 -- | Resource data.
53 , cachedData :: a
54 } deriving (Show, Eq)
55
56-- INVARIANT: minUpdateInterval <= updateInterval
57
58instance Default (Cached a) where
59 def = mempty
60
61instance Functor Cached where
62 fmap f (Cached t i m a) = Cached t i m (f a)
63
64posixEpoch :: NominalDiffTime
65posixEpoch = 1000000000000000000000000000000000000000000000000000000
66
67instance Applicative Cached where
68 pure = Cached 0 posixEpoch posixEpoch
69 f <*> c = Cached
70 { lastUpdated = undefined
71 , minUpdateInterval = undefined
72 , updateInterval = undefined
73 , cachedData = cachedData f (cachedData c)
74 }
75
76instance Alternative Cached where
77 empty = mempty
78 (<|>) = error "cached alternative instance: not implemented"
79
80instance Monad Cached where
81 return = pure
82 Cached {..} >>= f = Cached
83 { lastUpdated = undefined
84 , updateInterval = undefined
85 , minUpdateInterval = undefined
86 , cachedData = undefined
87 }
88
89instance Monoid (Cached a) where
90 mempty = Cached
91 { lastUpdated = 0
92 , minUpdateInterval = 0
93 , updateInterval = 0
94 , cachedData = error "cached mempty: impossible happen"
95 }
96
97 mappend a b
98 | expirationTime a > expirationTime b = a
99 | otherwise = b
100
101normalize :: NominalDiffTime -> NominalDiffTime
102 -> (NominalDiffTime, NominalDiffTime)
103normalize a b
104 | a < b = (a, b)
105 | otherwise = (b, a)
106{-# INLINE normalize #-}
107
108newCached :: NominalDiffTime -> NominalDiffTime -> a -> IO (Cached a)
109newCached minInterval interval x = do
110 t <- getPOSIXTime
111 let (mui, ui) = normalize minInterval interval
112 return Cached
113 { lastUpdated = t
114 , minUpdateInterval = mui
115 , updateInterval = ui
116 , cachedData = x
117 }
118
119newCached_ :: NominalDiffTime -> a -> IO (Cached a)
120newCached_ interval x = newCached interval interval x
121{-# INLINE newCached_ #-}
122
123expirationTime :: Cached a -> POSIXTime
124expirationTime Cached {..} = undefined
125
126isAlive :: Cached a -> IO Bool
127isAlive Cached {..} = do
128 currentTime <- getPOSIXTime
129 return $ lastUpdated + updateInterval > currentTime
130
131isExpired :: Cached a -> IO Bool
132isExpired Cached {..} = undefined
133
134isStalled :: Cached a -> IO Bool
135isStalled Cached {..} = undefined
136
137canUpdate :: Cached a -> IO (Maybe NominalDiffTime)
138canUpdate = undefined --isStaled
139
140shouldUpdate :: Cached a -> IO (Maybe NominalDiffTime)
141shouldUpdate = undefined -- isExpired
142
143tryTakeData :: Cached a -> IO (Maybe a)
144tryTakeData c = do
145 alive <- isAlive c
146 return $ if alive then Just (cachedData c) else Nothing
147
148unsafeTryTakeData :: Cached a -> Maybe a
149unsafeTryTakeData = unsafePerformIO . tryTakeData
150
151invalidateData :: Cached a -> IO a -> IO (Cached a)
152invalidateData Cached {..} action = do
153 t <- getPOSIXTime
154 x <- action
155 return Cached
156 { lastUpdated = t
157 , updateInterval = updateInterval
158 , minUpdateInterval = minUpdateInterval
159 , cachedData = x
160 }
161
162takeData :: Cached a -> IO a -> IO a
163takeData c action = do
164 mdata <- tryTakeData c
165 case mdata of
166 Just a -> return a
167 Nothing -> do
168 c' <- invalidateData c action
169 takeData c' action
diff --git a/bittorrent/src/Network/BitTorrent/Internal/Progress.hs b/bittorrent/src/Network/BitTorrent/Internal/Progress.hs
new file mode 100644
index 00000000..6ac889e2
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Internal/Progress.hs
@@ -0,0 +1,154 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- 'Progress' used to track amount downloaded\/left\/upload bytes
9-- either on per client or per torrent basis. This value is used to
10-- notify the tracker and usually shown to the user. To aggregate
11-- total progress you can use the Monoid instance.
12--
13{-# LANGUAGE TemplateHaskell #-}
14{-# LANGUAGE ViewPatterns #-}
15{-# OPTIONS -fno-warn-orphans #-}
16module Network.BitTorrent.Internal.Progress
17 ( -- * Progress
18 Progress (..)
19
20 -- * Lens
21 , left
22 , uploaded
23 , downloaded
24
25 -- * Construction
26 , startProgress
27 , downloadedProgress
28 , enqueuedProgress
29 , uploadedProgress
30 , dequeuedProgress
31
32 -- * Query
33 , canDownload
34 , canUpload
35 ) where
36
37import Control.Applicative
38import Control.Lens hiding ((%=))
39import Data.ByteString.Lazy.Builder as BS
40import Data.ByteString.Lazy.Builder.ASCII as BS
41import Data.Default
42import Data.Monoid
43import Data.Serialize as S
44import Data.Ratio
45import Data.Word
46import Network.HTTP.Types.QueryLike
47import Text.PrettyPrint as PP
48import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
49
50
51-- | Progress data is considered as dynamic within one client
52-- session. This data also should be shared across client application
53-- sessions (e.g. files), otherwise use 'startProgress' to get initial
54-- 'Progress' value.
55--
56data Progress = Progress
57 { _downloaded :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes downloaded;
58 , _left :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes left;
59 , _uploaded :: {-# UNPACK #-} !Word64 -- ^ Total amount of bytes uploaded.
60 } deriving (Show, Read, Eq)
61
62$(makeLenses ''Progress)
63
64-- | UDP tracker compatible encoding.
65instance Serialize Progress where
66 put Progress {..} = do
67 putWord64be $ fromIntegral _downloaded
68 putWord64be $ fromIntegral _left
69 putWord64be $ fromIntegral _uploaded
70
71 get = Progress
72 <$> (fromIntegral <$> getWord64be)
73 <*> (fromIntegral <$> getWord64be)
74 <*> (fromIntegral <$> getWord64be)
75
76instance Default Progress where
77 def = Progress 0 0 0
78 {-# INLINE def #-}
79
80-- | Can be used to aggregate total progress.
81instance Monoid Progress where
82 mempty = def
83 {-# INLINE mempty #-}
84
85 mappend (Progress da la ua) (Progress db lb ub) = Progress
86 { _downloaded = da + db
87 , _left = la + lb
88 , _uploaded = ua + ub
89 }
90 {-# INLINE mappend #-}
91
92instance QueryValueLike Builder where
93 toQueryValue = toQueryValue . BS.toLazyByteString
94
95instance QueryValueLike Word64 where
96 toQueryValue = toQueryValue . BS.word64Dec
97
98-- | HTTP Tracker protocol compatible encoding.
99instance QueryLike Progress where
100 toQuery Progress {..} =
101 [ ("uploaded" , toQueryValue _uploaded)
102 , ("left" , toQueryValue _left)
103 , ("downloaded", toQueryValue _downloaded)
104 ]
105
106instance Pretty Progress where
107 pPrint Progress {..} =
108 "/\\" <+> PP.text (show _uploaded) $$
109 "\\/" <+> PP.text (show _downloaded) $$
110 "left" <+> PP.text (show _left)
111
112-- | Initial progress is used when there are no session before.
113--
114-- Please note that tracker might penalize client some way if the do
115-- not accumulate progress. If possible and save 'Progress' between
116-- client sessions to avoid that.
117--
118startProgress :: Integer -> Progress
119startProgress = Progress 0 0 . fromIntegral
120{-# INLINE startProgress #-}
121
122-- | Used when the client download some data from /any/ peer.
123downloadedProgress :: Int -> Progress -> Progress
124downloadedProgress (fromIntegral -> amount)
125 = (left -~ amount)
126 . (downloaded +~ amount)
127{-# INLINE downloadedProgress #-}
128
129-- | Used when the client upload some data to /any/ peer.
130uploadedProgress :: Int -> Progress -> Progress
131uploadedProgress (fromIntegral -> amount) = uploaded +~ amount
132{-# INLINE uploadedProgress #-}
133
134-- | Used when leecher join client session.
135enqueuedProgress :: Integer -> Progress -> Progress
136enqueuedProgress amount = left +~ fromIntegral amount
137{-# INLINE enqueuedProgress #-}
138
139-- | Used when leecher leave client session.
140-- (e.g. user deletes not completed torrent)
141dequeuedProgress :: Integer -> Progress -> Progress
142dequeuedProgress amount = left -~ fromIntegral amount
143{-# INLINE dequeuedProgress #-}
144
145ri2rw64 :: Ratio Int -> Ratio Word64
146ri2rw64 x = fromIntegral (numerator x) % fromIntegral (denominator x)
147
148-- | Check global /download/ limit by uploaded \/ downloaded ratio.
149canDownload :: Ratio Int -> Progress -> Bool
150canDownload limit Progress {..} = _uploaded % _downloaded > ri2rw64 limit
151
152-- | Check global /upload/ limit by downloaded \/ uploaded ratio.
153canUpload :: Ratio Int -> Progress -> Bool
154canUpload limit Progress {..} = _downloaded % _uploaded > ri2rw64 limit
diff --git a/bittorrent/src/Network/BitTorrent/Internal/Types.hs b/bittorrent/src/Network/BitTorrent/Internal/Types.hs
new file mode 100644
index 00000000..d157db3e
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Internal/Types.hs
@@ -0,0 +1,10 @@
1{-# LANGUAGE TypeFamilies #-}
2module Network.BitTorrent.Internal.Types
3 ( EventSource (..)
4 ) where
5
6import Control.Concurrent.Chan.Split
7
8class EventSource source where
9 data Event source
10 listen :: source -> IO (ReceivePort (Event source))
diff --git a/bittorrent/src/Network/BitTorrent/Readme.md b/bittorrent/src/Network/BitTorrent/Readme.md
new file mode 100644
index 00000000..ebf9545e
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Readme.md
@@ -0,0 +1,10 @@
1Layout
2======
3
4| module group | can import | main purpose |
5|:-------------|:------------:|:--------------------------------------:|
6| Core | | common datatypes |
7| DHT | Core | centralized peer discovery |
8| Tracker | Core | decentralized peer discovery |
9| Exchange | Core | torrent content exchange |
10| Client | any other | core of bittorrent client application |
diff --git a/bittorrent/src/Network/BitTorrent/Tracker.hs b/bittorrent/src/Network/BitTorrent/Tracker.hs
new file mode 100644
index 00000000..6db67559
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker.hs
@@ -0,0 +1,50 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : non-portable
7--
8-- This module provides high level API for peer -> tracker
9-- communication. Tracker is used to discover other peers in the
10-- network using torrent info hash.
11--
12{-# LANGUAGE TemplateHaskell #-}
13module Network.BitTorrent.Tracker
14 ( -- * RPC Manager
15 PeerInfo (..)
16 , Options
17 , Manager
18 , newManager
19 , closeManager
20 , withManager
21
22 -- * Multitracker session
23 , trackerList
24 , Session
25 , Event (..)
26 , newSession
27 , closeSession
28 , withSession
29
30 -- ** Events
31 , AnnounceEvent (..)
32 , notify
33 , askPeers
34
35 -- ** Session state
36 , TrackerSession
37 , trackerPeers
38 , trackerScrape
39
40 , tryTakeData
41 , unsafeTryTakeData
42
43 , getSessionState
44 ) where
45
46import Network.BitTorrent.Internal.Cache (tryTakeData, unsafeTryTakeData)
47import Network.BitTorrent.Tracker.Message
48import Network.BitTorrent.Tracker.List
49import Network.BitTorrent.Tracker.RPC
50import Network.BitTorrent.Tracker.Session
diff --git a/bittorrent/src/Network/BitTorrent/Tracker/List.hs b/bittorrent/src/Network/BitTorrent/Tracker/List.hs
new file mode 100644
index 00000000..0eb11641
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker/List.hs
@@ -0,0 +1,193 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Multitracker Metadata Extension support.
9--
10-- For more info see: <http://www.bittorrent.org/beps/bep_0012.html>
11--
12{-# LANGUAGE FlexibleInstances #-}
13module Network.BitTorrent.Tracker.List
14 ( -- * Tracker list
15 TierEntry
16 , TrackerList
17
18 -- * Construction
19 , trackerList
20 , shuffleTiers
21 , mapWithURI
22 , Network.BitTorrent.Tracker.List.toList
23
24 -- * Traversals
25 , traverseAll
26 , traverseTiers
27 ) where
28
29import Prelude hiding (mapM, foldr)
30import Control.Arrow
31import Control.Applicative
32import Control.Exception
33import Data.Default
34import Data.List as L (map, elem, any, filter, null)
35import Data.Maybe
36import Data.Foldable
37import Data.Traversable
38import Network.URI
39import System.Random.Shuffle
40
41import Data.Torrent
42import Network.BitTorrent.Tracker.RPC as RPC
43
44{-----------------------------------------------------------------------
45-- Tracker list datatype
46-----------------------------------------------------------------------}
47
48type TierEntry a = (URI, a)
49type Tier a = [TierEntry a]
50
51-- | Tracker list is either a single tracker or list of tiers. All
52-- trackers in each tier must be checked before the client goes on to
53-- the next tier.
54data TrackerList a
55 = Announce (TierEntry a) -- ^ torrent file 'announce' field only
56 | TierList [Tier a] -- ^ torrent file 'announce-list' field only
57 deriving (Show, Eq)
58
59-- | Empty tracker list. Can be used for trackerless torrents.
60instance Default (TrackerList a) where
61 def = TierList []
62
63instance Functor TrackerList where
64 fmap f (Announce (uri, a)) = Announce (uri, f a)
65 fmap f (TierList a) = TierList (fmap (fmap (second f)) a)
66
67instance Foldable TrackerList where
68 foldr f z (Announce e ) = f (snd e) z
69 foldr f z (TierList xs) = foldr (flip (foldr (f . snd))) z xs
70
71_traverseEntry f (uri, a) = (,) uri <$> f a
72
73instance Traversable TrackerList where
74 traverse f (Announce e ) = Announce <$> _traverseEntry f e
75 traverse f (TierList xs) =
76 TierList <$> traverse (traverse (_traverseEntry f)) xs
77
78traverseWithURI :: Applicative f
79 => (TierEntry a -> f b) -> TrackerList a -> f (TrackerList b)
80traverseWithURI f (Announce (uri, a)) = (Announce . (,) uri) <$> f (uri, a)
81traverseWithURI f (TierList xxs ) =
82 TierList <$> traverse (traverse (traverseEntry f)) xxs
83 where
84 traverseEntry f (uri, a) = (,) uri <$> f (uri, a)
85
86{-----------------------------------------------------------------------
87-- List extraction
88-----------------------------------------------------------------------}
89-- BEP12 do not expose any restrictions for the content of
90-- 'announce-list' key - there are some /bad/ cases can happen with
91-- poorly designed or even malicious torrent creation software.
92--
93-- Bad case #1: announce-list is present, but empty.
94--
95-- { tAnnounce = Just "http://a.com"
96-- , tAnnounceList = Just [[]]
97-- }
98--
99-- Bad case #2: announce uri do not present in announce list.
100--
101-- { tAnnounce = Just "http://a.com"
102-- , tAnnounceList = Just [["udp://a.com"]]
103-- }
104--
105-- The addBackup function solves both problems by adding announce uri
106-- as backup tier.
107--
108addBackup :: [[URI]] -> URI -> [[URI]]
109addBackup tiers bkp
110 | L.any (L.elem bkp) tiers = tiers
111 | otherwise = tiers ++ [[bkp]]
112
113fixList :: Maybe [[URI]] -> Maybe URI -> Maybe [[URI]]
114fixList mxss mx = do
115 xss <- mxss
116 let xss' = L.filter (not . L.null) xss
117 return $ maybe xss' (addBackup xss') mx
118
119-- | Extract set of trackers from torrent file. The 'tAnnounce' key is
120-- only ignored if the 'tAnnounceList' key is present.
121trackerList :: Torrent -> TrackerList ()
122trackerList Torrent {..} = fromMaybe (TierList []) $ do
123 (TierList . tierList) <$> (tAnnounceList `fixList` tAnnounce)
124 <|> (Announce . nullEntry) <$> tAnnounce
125 where
126 nullEntry uri = (uri, ())
127 tierList = L.map (L.map nullEntry)
128
129-- | Shuffle /order of trackers/ in each tier, preserving original
130-- /order of tiers/. This can help to balance the load between the
131-- trackers.
132shuffleTiers :: TrackerList a -> IO (TrackerList a)
133shuffleTiers (Announce a ) = return (Announce a)
134shuffleTiers (TierList xs) = TierList <$> mapM shuffleM xs
135
136mapWithURI :: (URI -> a -> b) -> TrackerList a -> TrackerList b
137mapWithURI f (Announce (uri, a)) = Announce (uri, f uri a)
138mapWithURI f (TierList xs ) = TierList (L.map (L.map mapEntry) xs)
139 where
140 mapEntry (uri, a) = (uri, f uri a)
141
142toList :: TrackerList a -> [[TierEntry a]]
143toList (Announce e) = [[e]]
144toList (TierList xxs) = xxs
145
146{-----------------------------------------------------------------------
147-- Special traversals (suppressed RPC exceptions)
148-----------------------------------------------------------------------}
149
150catchRPC :: IO a -> IO a -> IO a
151catchRPC a b = catch a (f b)
152 where
153 f :: a -> RpcException -> a
154 f = const
155
156throwRPC :: String -> IO a
157throwRPC = throwIO . GenericException
158
159-- | Like 'traverse' but ignores 'RpcExceptions'.
160traverseAll :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a)
161traverseAll action = traverseWithURI (action $?)
162 where
163 f $? x = catchRPC (f x) (return (snd x))
164
165-- | Like 'traverse' but put working trackers to the head of tiers.
166-- This can help to avoid exceessive requests to not available
167-- trackers at each reannounce. If no one action succeed then original
168-- list is returned.
169traverseTiers :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a)
170traverseTiers action ts = catchRPC (goList ts) (return ts)
171 where
172 goList tl @ (Announce _ ) = traverseWithURI action tl
173 goList (TierList tiers) = TierList <$> goTiers (goTier []) tiers
174
175 goTiers _ [] = throwRPC "traverseTiers: no tiers"
176 goTiers f (x : xs) = catchRPC shortcut failback
177 where
178 shortcut = do
179 x' <- f x
180 return (x' : xs)
181
182 failback = do
183 xs' <- goTiers f xs
184 return (x : xs')
185
186 goTier _ [] = throwRPC "traverseTiers: no trackers in tier"
187 goTier failed ((uri, a) : as) = catchRPC shortcut failback
188 where
189 shortcut = do
190 a' <- action (uri, a)
191 return ((uri, a') : as ++ failed) -- failed trackers at the end
192
193 failback = goTier ((uri, a) : failed) as
diff --git a/bittorrent/src/Network/BitTorrent/Tracker/Message.hs b/bittorrent/src/Network/BitTorrent/Tracker/Message.hs
new file mode 100644
index 00000000..b9b6a9d3
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker/Message.hs
@@ -0,0 +1,920 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- (c) Daniel Gröber 2013
4-- License : BSD3
5-- Maintainer : pxqr.sta@gmail.com
6-- Stability : experimental
7-- Portability : portable
8--
9-- Every tracker should support announce query. This query is used
10-- to discover peers within a swarm and have two-fold effect:
11--
12-- * peer doing announce discover other peers using peer list from
13-- the response to the announce query.
14--
15-- * tracker store peer information and use it in the succeeding
16-- requests made by other peers, until the peer info expires.
17--
18-- By convention most trackers support another form of request —
19-- scrape query — which queries the state of a given torrent (or
20-- a list of torrents) that the tracker is managing.
21--
22{-# LANGUAGE FlexibleContexts #-}
23{-# LANGUAGE FlexibleInstances #-}
24{-# LANGUAGE GeneralizedNewtypeDeriving #-}
25{-# LANGUAGE TemplateHaskell #-}
26{-# LANGUAGE DeriveDataTypeable #-}
27{-# LANGUAGE DeriveFunctor #-}
28{-# LANGUAGE ScopedTypeVariables #-}
29{-# LANGUAGE TypeFamilies #-}
30{-# OPTIONS -fno-warn-orphans #-}
31module Network.BitTorrent.Tracker.Message
32 ( -- * Announce
33 -- ** Query
34 AnnounceEvent (..)
35 , AnnounceQuery (..)
36 , renderAnnounceQuery
37 , ParamParseFailure
38 , parseAnnounceQuery
39
40 -- ** Info
41 , PeerList (..)
42 , getPeerList
43 , AnnounceInfo(..)
44 , defaultNumWant
45 , defaultMaxNumWant
46 , defaultReannounceInterval
47
48 -- * Scrape
49 -- ** Query
50 , ScrapeQuery
51 , renderScrapeQuery
52 , parseScrapeQuery
53
54 -- ** Info
55 , ScrapeEntry (..)
56 , ScrapeInfo
57
58 -- * HTTP specific
59 -- ** Routes
60 , PathPiece
61 , defaultAnnouncePath
62 , defaultScrapePath
63
64 -- ** Preferences
65 , AnnouncePrefs (..)
66 , renderAnnouncePrefs
67 , parseAnnouncePrefs
68
69 -- ** Request
70 , AnnounceRequest (..)
71 , parseAnnounceRequest
72 , renderAnnounceRequest
73
74 -- ** Response
75 , announceType
76 , scrapeType
77 , parseFailureStatus
78
79 -- ** Extra
80 , queryToSimpleQuery
81
82 -- * UDP specific
83 -- ** Connection
84 , ConnectionId
85 , initialConnectionId
86
87 -- ** Messages
88 , Request (..)
89 , Response (..)
90 , responseName
91
92 -- ** Transaction
93 , genTransactionId
94 , TransactionId
95 , Transaction (..)
96 )
97 where
98
99import Control.Applicative
100import Control.Monad
101import Data.BEncode as BE hiding (Result)
102import Data.BEncode.BDict as BE
103import Data.ByteString as BS
104import Data.ByteString.Char8 as BC
105import Data.Char as Char
106import Data.Convertible
107import Data.Default
108import Data.Either
109import Data.List as L
110import Data.Maybe
111import Data.Monoid
112import Data.Serialize as S hiding (Result)
113import Data.String
114import Data.Text (Text)
115import Data.Text.Encoding
116import Data.Typeable
117import Data.Word
118import Data.IP
119import Network
120import Network.HTTP.Types.QueryLike
121import Network.HTTP.Types.URI hiding (urlEncode)
122import Network.HTTP.Types.Status
123import Network.Socket hiding (Connected)
124import Numeric
125import System.Entropy
126import Text.Read (readMaybe)
127
128import Data.Torrent
129import Network.Address
130import Network.BitTorrent.Internal.Progress
131
132{-----------------------------------------------------------------------
133-- Events
134-----------------------------------------------------------------------}
135
136-- | Events are used to specify which kind of announce query is performed.
137data AnnounceEvent
138 -- | For the first request: when download first begins.
139 = Started
140
141 -- | This peer stopped downloading /and/ uploading the torrent or
142 -- just shutting down.
143 | Stopped
144
145 -- | This peer completed downloading the torrent. This only happen
146 -- right after last piece have been verified. No 'Completed' is
147 -- sent if the file was completed when 'Started'.
148 | Completed
149 deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable)
150
151-- | HTTP tracker protocol compatible encoding.
152instance QueryValueLike AnnounceEvent where
153 toQueryValue e = toQueryValue (Char.toLower x : xs)
154 where
155 (x : xs) = show e -- INVARIANT: this is always nonempty list
156
157type EventId = Word32
158
159-- | UDP tracker encoding event codes.
160eventId :: AnnounceEvent -> EventId
161eventId Completed = 1
162eventId Started = 2
163eventId Stopped = 3
164
165-- TODO add Regular event
166putEvent :: Putter (Maybe AnnounceEvent)
167putEvent Nothing = putWord32be 0
168putEvent (Just e) = putWord32be (eventId e)
169
170getEvent :: S.Get (Maybe AnnounceEvent)
171getEvent = do
172 eid <- getWord32be
173 case eid of
174 0 -> return Nothing
175 1 -> return $ Just Completed
176 2 -> return $ Just Started
177 3 -> return $ Just Stopped
178 _ -> fail "unknown event id"
179
180{-----------------------------------------------------------------------
181 Announce query
182-----------------------------------------------------------------------}
183-- TODO add &ipv6= and &ipv4= params to AnnounceQuery
184-- http://www.bittorrent.org/beps/bep_0007.html#announce-parameter
185
186-- | A tracker request is HTTP GET request; used to include metrics
187-- from clients that help the tracker keep overall statistics about
188-- the torrent. The most important, requests are used by the tracker
189-- to keep track lists of active peer for a particular torrent.
190--
191data AnnounceQuery = AnnounceQuery
192 {
193 -- | Hash of info part of the torrent usually obtained from
194 -- 'Torrent' or 'Magnet'.
195 reqInfoHash :: !InfoHash
196
197 -- | ID of the peer doing request.
198 , reqPeerId :: !PeerId
199
200 -- | Port to listen to for connections from other
201 -- peers. Tracker should respond with this port when
202 -- some /other/ peer request the tracker with the same info hash.
203 -- Normally, this port is choosed from 'defaultPorts'.
204 , reqPort :: !PortNumber
205
206 -- | Current progress of peer doing request.
207 , reqProgress :: !Progress
208
209 -- | The peer IP. Needed only when client communicated with
210 -- tracker throught a proxy.
211 , reqIP :: Maybe HostAddress
212
213 -- | Number of peers that the peers wants to receive from. It is
214 -- optional for trackers to honor this limit. See note for
215 -- 'defaultNumWant'.
216 , reqNumWant :: Maybe Int
217
218 -- | If not specified, the request is regular periodic
219 -- request. Regular request should be sent
220 , reqEvent :: Maybe AnnounceEvent
221 } deriving (Show, Eq, Typeable)
222
223-- | UDP tracker protocol compatible encoding.
224instance Serialize AnnounceQuery where
225 put AnnounceQuery {..} = do
226 put reqInfoHash
227 put reqPeerId
228 put reqProgress
229 putEvent reqEvent
230 putWord32host $ fromMaybe 0 reqIP
231 putWord32be $ 0 -- TODO what the fuck is "key"?
232 putWord32be $ fromIntegral $ fromMaybe (-1) reqNumWant
233
234 put reqPort
235
236 get = do
237 ih <- get
238 pid <- get
239
240 progress <- get
241
242 ev <- getEvent
243 ip <- getWord32be
244-- key <- getWord32be -- TODO
245 want <- getWord32be
246
247 port <- get
248
249 return $ AnnounceQuery {
250 reqInfoHash = ih
251 , reqPeerId = pid
252 , reqPort = port
253 , reqProgress = progress
254 , reqIP = if ip == 0 then Nothing else Just ip
255 , reqNumWant = if want == -1 then Nothing
256 else Just (fromIntegral want)
257 , reqEvent = ev
258 }
259
260instance QueryValueLike PortNumber where
261 toQueryValue = toQueryValue . show . fromEnum
262
263instance QueryValueLike Word32 where
264 toQueryValue = toQueryValue . show
265
266instance QueryValueLike Int where
267 toQueryValue = toQueryValue . show
268
269-- | HTTP tracker protocol compatible encoding.
270instance QueryLike AnnounceQuery where
271 toQuery AnnounceQuery {..} =
272 toQuery reqProgress ++
273 [ ("info_hash", toQueryValue reqInfoHash) -- TODO use 'paramName'
274 , ("peer_id" , toQueryValue reqPeerId)
275 , ("port" , toQueryValue reqPort)
276 , ("ip" , toQueryValue reqIP)
277 , ("numwant" , toQueryValue reqNumWant)
278 , ("event" , toQueryValue reqEvent)
279 ]
280
281-- | Filter @param=value@ pairs with the unset value.
282queryToSimpleQuery :: Query -> SimpleQuery
283queryToSimpleQuery = catMaybes . L.map f
284 where
285 f (_, Nothing) = Nothing
286 f (a, Just b ) = Just (a, b)
287
288-- | Encode announce query to query string.
289renderAnnounceQuery :: AnnounceQuery -> SimpleQuery
290renderAnnounceQuery = queryToSimpleQuery . toQuery
291
292data QueryParam
293 -- announce query
294 = ParamInfoHash
295 | ParamPeerId
296 | ParamPort
297 | ParamUploaded
298 | ParamLeft
299 | ParamDownloaded
300 | ParamIP
301 | ParamNumWant
302 | ParamEvent
303 -- announce query ext
304 | ParamCompact
305 | ParamNoPeerId
306 deriving (Show, Eq, Ord, Enum)
307
308paramName :: QueryParam -> BS.ByteString
309paramName ParamInfoHash = "info_hash"
310paramName ParamPeerId = "peer_id"
311paramName ParamPort = "port"
312paramName ParamUploaded = "uploaded"
313paramName ParamLeft = "left"
314paramName ParamDownloaded = "downloaded"
315paramName ParamIP = "ip"
316paramName ParamNumWant = "numwant"
317paramName ParamEvent = "event"
318paramName ParamCompact = "compact"
319paramName ParamNoPeerId = "no_peer_id"
320{-# INLINE paramName #-}
321
322class FromParam a where
323 fromParam :: BS.ByteString -> Maybe a
324
325instance FromParam Bool where
326 fromParam "0" = Just False
327 fromParam "1" = Just True
328 fromParam _ = Nothing
329
330instance FromParam InfoHash where
331 fromParam = either (const Nothing) pure . safeConvert
332
333instance FromParam PeerId where
334 fromParam = either (const Nothing) pure . safeConvert
335
336instance FromParam Word32 where
337 fromParam = readMaybe . BC.unpack
338
339instance FromParam Word64 where
340 fromParam = readMaybe . BC.unpack
341
342instance FromParam Int where
343 fromParam = readMaybe . BC.unpack
344
345instance FromParam PortNumber where
346 fromParam bs = fromIntegral <$> (fromParam bs :: Maybe Word32)
347
348instance FromParam AnnounceEvent where
349 fromParam bs = do
350 (x, xs) <- BC.uncons bs
351 readMaybe $ BC.unpack $ BC.cons (Char.toUpper x) xs
352
353-- | 'ParamParseFailure' represent errors can occur while parsing HTTP
354-- tracker requests. In case of failure, this can be used to provide
355-- more informative 'statusCode' and 'statusMessage' in tracker
356-- responses.
357--
358data ParamParseFailure
359 = Missing QueryParam -- ^ param not found in query string;
360 | Invalid QueryParam BS.ByteString -- ^ param present but not valid.
361 deriving (Show, Eq)
362
363type ParseResult = Either ParamParseFailure
364
365withError :: ParamParseFailure -> Maybe a -> ParseResult a
366withError e = maybe (Left e) Right
367
368reqParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult a
369reqParam param xs = do
370 val <- withError (Missing param) $ L.lookup (paramName param) xs
371 withError (Invalid param val) (fromParam val)
372
373optParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult (Maybe a)
374optParam param ps
375 | Just x <- L.lookup (paramName param) ps
376 = pure <$> withError (Invalid param x) (fromParam x)
377 | otherwise = pure Nothing
378
379parseProgress :: SimpleQuery -> ParseResult Progress
380parseProgress params = Progress
381 <$> reqParam ParamDownloaded params
382 <*> reqParam ParamLeft params
383 <*> reqParam ParamUploaded params
384
385-- | Parse announce request from a query string.
386parseAnnounceQuery :: SimpleQuery -> ParseResult AnnounceQuery
387parseAnnounceQuery params = AnnounceQuery
388 <$> reqParam ParamInfoHash params
389 <*> reqParam ParamPeerId params
390 <*> reqParam ParamPort params
391 <*> parseProgress params
392 <*> optParam ParamIP params
393 <*> optParam ParamNumWant params
394 <*> optParam ParamEvent params
395
396{-----------------------------------------------------------------------
397-- Announce Info
398-----------------------------------------------------------------------}
399-- TODO check if announceinterval/complete/incomplete is positive ints
400
401-- | Tracker can return peer list in either compact(BEP23) or not
402-- compact form.
403--
404-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
405--
406data PeerList ip
407 = PeerList [PeerAddr IP]
408 | CompactPeerList [PeerAddr ip]
409 deriving (Show, Eq, Typeable, Functor)
410
411-- | The empty non-compact peer list.
412instance Default (PeerList IP) where
413 def = PeerList []
414 {-# INLINE def #-}
415
416getPeerList :: PeerList IP -> [PeerAddr IP]
417getPeerList (PeerList xs) = xs
418getPeerList (CompactPeerList xs) = xs
419
420instance Serialize a => BEncode (PeerList a) where
421 toBEncode (PeerList xs) = toBEncode xs
422 toBEncode (CompactPeerList xs) = toBEncode $ runPut (mapM_ put xs)
423
424 fromBEncode (BList l ) = PeerList <$> fromBEncode (BList l)
425 fromBEncode (BString s ) = CompactPeerList <$> runGet (many get) s
426 fromBEncode _ = decodingError "PeerList: should be a BString or BList"
427
428-- | The tracker response includes a peer list that helps the client
429-- participate in the torrent. The most important is 'respPeer' list
430-- used to join the swarm.
431--
432data AnnounceInfo =
433 Failure !Text -- ^ Failure reason in human readable form.
434 | AnnounceInfo {
435 -- | Number of peers completed the torrent. (seeders)
436 respComplete :: !(Maybe Int)
437
438 -- | Number of peers downloading the torrent. (leechers)
439 , respIncomplete :: !(Maybe Int)
440
441 -- | Recommended interval to wait between requests, in seconds.
442 , respInterval :: !Int
443
444 -- | Minimal amount of time between requests, in seconds. A
445 -- peer /should/ make timeout with at least 'respMinInterval'
446 -- value, otherwise tracker might not respond. If not specified
447 -- the same applies to 'respInterval'.
448 , respMinInterval :: !(Maybe Int)
449
450 -- | Peers that must be contacted.
451 , respPeers :: !(PeerList IP)
452
453 -- | Human readable warning.
454 , respWarning :: !(Maybe Text)
455 } deriving (Show, Eq, Typeable)
456
457-- | Empty peer list with default reannounce interval.
458instance Default AnnounceInfo where
459 def = AnnounceInfo
460 { respComplete = Nothing
461 , respIncomplete = Nothing
462 , respInterval = defaultReannounceInterval
463 , respMinInterval = Nothing
464 , respPeers = def
465 , respWarning = Nothing
466 }
467
468-- | HTTP tracker protocol compatible encoding.
469instance BEncode AnnounceInfo where
470 toBEncode (Failure t) = toDict $
471 "failure reason" .=! t
472 .: endDict
473
474 toBEncode AnnounceInfo {..} = toDict $
475 "complete" .=? respComplete
476 .: "incomplete" .=? respIncomplete
477 .: "interval" .=! respInterval
478 .: "min interval" .=? respMinInterval
479 .: "peers" .=! peers
480 .: "peers6" .=? peers6
481 .: "warning message" .=? respWarning
482 .: endDict
483 where
484 (peers, peers6) = prttn respPeers
485
486 prttn :: PeerList IP -> (PeerList IPv4, Maybe (PeerList IPv6))
487 prttn (PeerList xs) = (PeerList xs, Nothing)
488 prttn (CompactPeerList xs) = mk $ partitionEithers $ toEither <$> xs
489 where
490 mk (v4s, v6s)
491 | L.null v6s = (CompactPeerList v4s, Nothing)
492 | otherwise = (CompactPeerList v4s, Just (CompactPeerList v6s))
493
494 toEither :: PeerAddr IP -> Either (PeerAddr IPv4) (PeerAddr IPv6)
495 toEither PeerAddr {..} = case peerHost of
496 IPv4 ipv4 -> Left $ PeerAddr peerId ipv4 peerPort
497 IPv6 ipv6 -> Right $ PeerAddr peerId ipv6 peerPort
498
499 fromBEncode (BDict d)
500 | Just t <- BE.lookup "failure reason" d = Failure <$> fromBEncode t
501 | otherwise = (`fromDict` (BDict d)) $
502 AnnounceInfo
503 <$>? "complete"
504 <*>? "incomplete"
505 <*>! "interval"
506 <*>? "min interval"
507 <*> (uncurry merge =<< (,) <$>! "peers" <*>? "peers6")
508 <*>? "warning message"
509 where
510 merge :: PeerList IPv4 -> Maybe (PeerList IPv6) -> BE.Get (PeerList IP)
511 merge (PeerList ips) Nothing = pure (PeerList ips)
512 merge (PeerList _ ) (Just _)
513 = fail "PeerList: non-compact peer list provided, \
514 \but the `peers6' field present"
515
516 merge (CompactPeerList ipv4s) Nothing
517 = pure $ CompactPeerList (fmap IPv4 <$> ipv4s)
518
519 merge (CompactPeerList _ ) (Just (PeerList _))
520 = fail "PeerList: the `peers6' field value \
521 \should contain *compact* peer list"
522
523 merge (CompactPeerList ipv4s) (Just (CompactPeerList ipv6s))
524 = pure $ CompactPeerList $
525 (fmap IPv4 <$> ipv4s) <> (fmap IPv6 <$> ipv6s)
526
527 fromBEncode _ = decodingError "Announce info"
528
529-- | UDP tracker protocol compatible encoding.
530instance Serialize AnnounceInfo where
531 put (Failure msg) = put $ encodeUtf8 msg
532 put AnnounceInfo {..} = do
533 putWord32be $ fromIntegral respInterval
534 putWord32be $ fromIntegral $ fromMaybe 0 respIncomplete
535 putWord32be $ fromIntegral $ fromMaybe 0 respComplete
536 forM_ (fmap ipv4 <$> getPeerList respPeers) put
537
538 get = do
539 interval <- getWord32be
540 leechers <- getWord32be
541 seeders <- getWord32be
542 peers <- many $ fmap IPv4 <$> get
543
544 return $ AnnounceInfo {
545 respWarning = Nothing
546 , respInterval = fromIntegral interval
547 , respMinInterval = Nothing
548 , respIncomplete = Just $ fromIntegral leechers
549 , respComplete = Just $ fromIntegral seeders
550 , respPeers = PeerList peers
551 }
552
553-- | Decodes announce response from bencoded string, for debugging only.
554instance IsString AnnounceInfo where
555 fromString str = either (error . format) id $ BE.decode (fromString str)
556 where
557 format msg = "fromString: unable to decode AnnounceInfo: " ++ msg
558
559-- | Above 25, new peers are highly unlikely to increase download
560-- speed. Even 30 peers is /plenty/, the official client version 3
561-- in fact only actively forms new connections if it has less than
562-- 30 peers and will refuse connections if it has 55.
563--
564-- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Basic_Tracker_Announce_Request>
565--
566defaultNumWant :: Int
567defaultNumWant = 50
568
569-- | Reasonable upper bound of numwant parameter.
570defaultMaxNumWant :: Int
571defaultMaxNumWant = 200
572
573-- | Widely used reannounce interval. Note: tracker clients should not
574-- use this value!
575defaultReannounceInterval :: Int
576defaultReannounceInterval = 30 * 60
577
578{-----------------------------------------------------------------------
579 Scrape message
580-----------------------------------------------------------------------}
581
582-- | Scrape query used to specify a set of torrent to scrape.
583-- If list is empty then tracker should return scrape info about each
584-- torrent.
585type ScrapeQuery = [InfoHash]
586
587-- TODO
588-- data ScrapeQuery
589-- = ScrapeAll
590-- | ScrapeSingle InfoHash
591-- | ScrapeMulti (HashSet InfoHash)
592-- deriving (Show)
593--
594-- data ScrapeInfo
595-- = ScrapeAll (HashMap InfoHash ScrapeEntry)
596-- | ScrapeSingle InfoHash ScrapeEntry
597-- | ScrapeMulti (HashMap InfoHash ScrapeEntry)
598--
599
600scrapeParam :: BS.ByteString
601scrapeParam = "info_hash"
602
603isScrapeParam :: BS.ByteString -> Bool
604isScrapeParam = (==) scrapeParam
605
606-- | Parse scrape query to query string.
607parseScrapeQuery :: SimpleQuery -> ScrapeQuery
608parseScrapeQuery
609 = catMaybes . L.map (fromParam . snd) . L.filter (isScrapeParam . fst)
610
611-- | Render scrape query to query string.
612renderScrapeQuery :: ScrapeQuery -> SimpleQuery
613renderScrapeQuery = queryToSimpleQuery . L.map mkPair
614 where
615 mkPair ih = (scrapeParam, toQueryValue ih)
616
617-- | Overall information about particular torrent.
618data ScrapeEntry = ScrapeEntry {
619 -- | Number of seeders - peers with the entire file.
620 siComplete :: {-# UNPACK #-} !Int
621
622 -- | Total number of times the tracker has registered a completion.
623 , siDownloaded :: {-# UNPACK #-} !Int
624
625 -- | Number of leechers.
626 , siIncomplete :: {-# UNPACK #-} !Int
627
628 -- | Name of the torrent file, as specified by the "name"
629 -- file in the info section of the .torrent file.
630 , siName :: !(Maybe Text)
631 } deriving (Show, Eq, Typeable)
632
633-- | HTTP tracker protocol compatible encoding.
634instance BEncode ScrapeEntry where
635 toBEncode ScrapeEntry {..} = toDict $
636 "complete" .=! siComplete
637 .: "downloaded" .=! siDownloaded
638 .: "incomplete" .=! siIncomplete
639 .: "name" .=? siName
640 .: endDict
641
642 fromBEncode = fromDict $ ScrapeEntry
643 <$>! "complete"
644 <*>! "downloaded"
645 <*>! "incomplete"
646 <*>? "name"
647
648-- | UDP tracker protocol compatible encoding.
649instance Serialize ScrapeEntry where
650 put ScrapeEntry {..} = do
651 putWord32be $ fromIntegral siComplete
652 putWord32be $ fromIntegral siDownloaded
653 putWord32be $ fromIntegral siIncomplete
654
655 get = ScrapeEntry
656 <$> (fromIntegral <$> getWord32be)
657 <*> (fromIntegral <$> getWord32be)
658 <*> (fromIntegral <$> getWord32be)
659 <*> pure Nothing
660
661-- | Scrape info about a set of torrents.
662type ScrapeInfo = [(InfoHash, ScrapeEntry)]
663
664{-----------------------------------------------------------------------
665-- HTTP specific
666-----------------------------------------------------------------------}
667
668-- | Some HTTP trackers allow to choose prefered representation of the
669-- 'AnnounceInfo'. It's optional for trackers to honor any of this
670-- options.
671data AnnouncePrefs = AnnouncePrefs
672 { -- | If specified, "compact" parameter is used to advise the
673 -- tracker to send peer id list as:
674 --
675 -- * bencoded list (extCompact = Just False);
676 -- * or more compact binary string (extCompact = Just True).
677 --
678 -- The later is prefered since compact peer list will reduce the
679 -- size of tracker responses. Hovewer, if tracker do not support
680 -- this extension then it can return peer list in either form.
681 --
682 -- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
683 --
684 extCompact :: !(Maybe Bool)
685
686 -- | If specified, "no_peer_id" parameter is used advise tracker
687 -- to either send or not to send peer id in tracker response.
688 -- Tracker may not support this extension as well.
689 --
690 -- For more info see:
691 -- <http://permalink.gmane.org/gmane.network.bit-torrent.general/4030>
692 --
693 , extNoPeerId :: !(Maybe Bool)
694 } deriving (Show, Eq, Typeable)
695
696instance Default AnnouncePrefs where
697 def = AnnouncePrefs Nothing Nothing
698
699instance QueryLike AnnouncePrefs where
700 toQuery AnnouncePrefs {..} =
701 [ ("compact", toQueryFlag <$> extCompact) -- TODO use 'paramName'
702 , ("no_peer_id", toQueryFlag <$> extNoPeerId)
703 ]
704 where
705 toQueryFlag False = "0"
706 toQueryFlag True = "1"
707
708-- | Parse announce query extended part from query string.
709parseAnnouncePrefs :: SimpleQuery -> AnnouncePrefs
710parseAnnouncePrefs params = either (const def) id $
711 AnnouncePrefs
712 <$> optParam ParamCompact params
713 <*> optParam ParamNoPeerId params
714
715-- | Render announce preferences to query string.
716renderAnnouncePrefs :: AnnouncePrefs -> SimpleQuery
717renderAnnouncePrefs = queryToSimpleQuery . toQuery
718
719-- | HTTP tracker request with preferences.
720data AnnounceRequest = AnnounceRequest
721 { announceQuery :: AnnounceQuery -- ^ Request query params.
722 , announcePrefs :: AnnouncePrefs -- ^ Optional advises to the tracker.
723 } deriving (Show, Eq, Typeable)
724
725instance QueryLike AnnounceRequest where
726 toQuery AnnounceRequest{..} =
727 toQuery announcePrefs <>
728 toQuery announceQuery
729
730-- | Parse announce request from query string.
731parseAnnounceRequest :: SimpleQuery -> ParseResult AnnounceRequest
732parseAnnounceRequest params = AnnounceRequest
733 <$> parseAnnounceQuery params
734 <*> pure (parseAnnouncePrefs params)
735
736-- | Render announce request to query string.
737renderAnnounceRequest :: AnnounceRequest -> SimpleQuery
738renderAnnounceRequest = queryToSimpleQuery . toQuery
739
740type PathPiece = BS.ByteString
741
742defaultAnnouncePath :: PathPiece
743defaultAnnouncePath = "announce"
744
745defaultScrapePath :: PathPiece
746defaultScrapePath = "scrape"
747
748missingOffset :: Int
749missingOffset = 101
750
751invalidOffset :: Int
752invalidOffset = 150
753
754parseFailureCode :: ParamParseFailure -> Int
755parseFailureCode (Missing param ) = missingOffset + fromEnum param
756parseFailureCode (Invalid param _) = invalidOffset + fromEnum param
757
758parseFailureMessage :: ParamParseFailure -> BS.ByteString
759parseFailureMessage e = BS.concat $ case e of
760 Missing p -> ["Missing parameter: ", paramName p]
761 Invalid p v -> ["Invalid parameter: ", paramName p, " = ", v]
762
763-- | HTTP response /content type/ for announce info.
764announceType :: ByteString
765announceType = "text/plain"
766
767-- | HTTP response /content type/ for scrape info.
768scrapeType :: ByteString
769scrapeType = "text/plain"
770
771-- | Get HTTP response status from a announce params parse failure.
772--
773-- For more info see:
774-- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Response_Codes>
775--
776parseFailureStatus :: ParamParseFailure -> Status
777parseFailureStatus = mkStatus <$> parseFailureCode <*> parseFailureMessage
778
779{-----------------------------------------------------------------------
780-- UDP specific message types
781-----------------------------------------------------------------------}
782
783genToken :: IO Word64
784genToken = do
785 bs <- getEntropy 8
786 either err return $ runGet getWord64be bs
787 where
788 err = error "genToken: impossible happen"
789
790-- | Connection Id is used for entire tracker session.
791newtype ConnectionId = ConnectionId Word64
792 deriving (Eq, Serialize)
793
794instance Show ConnectionId where
795 showsPrec _ (ConnectionId cid) = showString "0x" <> showHex cid
796
797initialConnectionId :: ConnectionId
798initialConnectionId = ConnectionId 0x41727101980
799
800-- | Transaction Id is used within a UDP RPC.
801newtype TransactionId = TransactionId Word32
802 deriving (Eq, Ord, Enum, Bounded, Serialize)
803
804instance Show TransactionId where
805 showsPrec _ (TransactionId tid) = showString "0x" <> showHex tid
806
807genTransactionId :: IO TransactionId
808genTransactionId = (TransactionId . fromIntegral) <$> genToken
809
810data Request
811 = Connect
812 | Announce AnnounceQuery
813 | Scrape ScrapeQuery
814 deriving Show
815
816data Response
817 = Connected ConnectionId
818 | Announced AnnounceInfo
819 | Scraped [ScrapeEntry]
820 | Failed Text
821 deriving Show
822
823responseName :: Response -> String
824responseName (Connected _) = "connected"
825responseName (Announced _) = "announced"
826responseName (Scraped _) = "scraped"
827responseName (Failed _) = "failed"
828
829data family Transaction a
830data instance Transaction Request = TransactionQ
831 { connIdQ :: {-# UNPACK #-} !ConnectionId
832 , transIdQ :: {-# UNPACK #-} !TransactionId
833 , request :: !Request
834 } deriving Show
835data instance Transaction Response = TransactionR
836 { transIdR :: {-# UNPACK #-} !TransactionId
837 , response :: !Response
838 } deriving Show
839
840-- TODO newtype
841newtype MessageId = MessageId Word32
842 deriving (Show, Eq, Num, Serialize)
843
844connectId, announceId, scrapeId, errorId :: MessageId
845connectId = 0
846announceId = 1
847scrapeId = 2
848errorId = 3
849
850instance Serialize (Transaction Request) where
851 put TransactionQ {..} = do
852 case request of
853 Connect -> do
854 put initialConnectionId
855 put connectId
856 put transIdQ
857
858 Announce ann -> do
859 put connIdQ
860 put announceId
861 put transIdQ
862 put ann
863
864 Scrape hashes -> do
865 put connIdQ
866 put scrapeId
867 put transIdQ
868 forM_ hashes put
869
870 get = do
871 cid <- get
872 mid <- get
873 TransactionQ cid <$> S.get <*> getBody mid
874 where
875 getBody :: MessageId -> S.Get Request
876 getBody msgId
877 | msgId == connectId = pure Connect
878 | msgId == announceId = Announce <$> get
879 | msgId == scrapeId = Scrape <$> many get
880 | otherwise = fail errMsg
881 where
882 errMsg = "unknown request: " ++ show msgId
883
884instance Serialize (Transaction Response) where
885 put TransactionR {..} = do
886 case response of
887 Connected conn -> do
888 put connectId
889 put transIdR
890 put conn
891
892 Announced info -> do
893 put announceId
894 put transIdR
895 put info
896
897 Scraped infos -> do
898 put scrapeId
899 put transIdR
900 forM_ infos put
901
902 Failed info -> do
903 put errorId
904 put transIdR
905 put (encodeUtf8 info)
906
907
908 get = do
909 mid <- get
910 TransactionR <$> get <*> getBody mid
911 where
912 getBody :: MessageId -> S.Get Response
913 getBody msgId
914 | msgId == connectId = Connected <$> get
915 | msgId == announceId = Announced <$> get
916 | msgId == scrapeId = Scraped <$> many get
917 | msgId == errorId = (Failed . decodeUtf8) <$> get
918 | otherwise = fail msg
919 where
920 msg = "unknown response: " ++ show msgId
diff --git a/bittorrent/src/Network/BitTorrent/Tracker/RPC.hs b/bittorrent/src/Network/BitTorrent/Tracker/RPC.hs
new file mode 100644
index 00000000..45fef05e
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker/RPC.hs
@@ -0,0 +1,175 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module provides unified RPC interface to BitTorrent
9-- trackers. The tracker is an UDP/HTTP/HTTPS service used to
10-- discovery peers for a particular existing torrent and keep
11-- statistics about the swarm. This module also provides a way to
12-- request scrape info for a particular torrent list.
13--
14{-# LANGUAGE DeriveDataTypeable #-}
15module Network.BitTorrent.Tracker.RPC
16 ( PeerInfo (..)
17
18 -- * Manager
19 , Options (..)
20 , Manager
21 , newManager
22 , closeManager
23 , withManager
24
25 -- * RPC
26 , SAnnounceQuery (..)
27 , RpcException (..)
28 , Network.BitTorrent.Tracker.RPC.announce
29 , scrape
30 ) where
31
32import Control.Exception
33import Data.Default
34import Data.Typeable
35import Network
36import Network.URI
37import Network.Socket (HostAddress)
38
39import Data.Torrent
40import Network.Address
41import Network.BitTorrent.Internal.Progress
42import Network.BitTorrent.Tracker.Message
43import qualified Network.BitTorrent.Tracker.RPC.HTTP as HTTP
44import qualified Network.BitTorrent.Tracker.RPC.UDP as UDP
45
46
47{-----------------------------------------------------------------------
48-- Simplified announce
49-----------------------------------------------------------------------}
50
51-- | Info to advertise to trackers.
52data PeerInfo = PeerInfo
53 { peerId :: !PeerId
54 , peerIP :: !(Maybe HostAddress)
55 , peerPort :: !PortNumber
56 } deriving (Show, Eq)
57
58instance Default PeerInfo where
59 def = PeerInfo def Nothing 6881
60
61-- | Simplified announce query.
62data SAnnounceQuery = SAnnounceQuery
63 { sInfoHash :: InfoHash
64 , sProgress :: Progress
65 , sNumWant :: Maybe Int
66 , sEvent :: Maybe AnnounceEvent
67 }
68
69fillAnnounceQuery :: PeerInfo -> SAnnounceQuery -> AnnounceQuery
70fillAnnounceQuery PeerInfo{..} SAnnounceQuery {..} = AnnounceQuery
71 { reqInfoHash = sInfoHash
72 , reqPeerId = peerId
73 , reqPort = peerPort
74 , reqProgress = sProgress
75 , reqIP = peerIP
76 , reqNumWant = sNumWant
77 , reqEvent = sEvent
78 }
79
80{-----------------------------------------------------------------------
81-- RPC manager
82-----------------------------------------------------------------------}
83
84-- | Tracker manager settings.
85data Options = Options
86 { -- | HTTP tracker protocol specific options.
87 optHttpRPC :: !HTTP.Options
88
89 -- | UDP tracker protocol specific options.
90 , optUdpRPC :: !UDP.Options
91
92 -- | Whether to use multitracker extension.
93 , optMultitracker :: !Bool
94 }
95
96instance Default Options where
97 def = Options
98 { optHttpRPC = def
99 , optUdpRPC = def
100 , optMultitracker = True
101 }
102
103-- | Tracker RPC Manager.
104data Manager = Manager
105 { options :: !Options
106 , peerInfo :: !PeerInfo
107 , httpMgr :: !HTTP.Manager
108 , udpMgr :: !UDP.Manager
109 }
110
111-- | Create a new 'Manager'. You /must/ manually 'closeManager'
112-- otherwise resource leakage is possible. Normally, a bittorrent
113-- client need a single RPC manager only.
114--
115-- This function can throw 'IOException' on invalid 'Options'.
116--
117newManager :: Options -> PeerInfo -> IO Manager
118newManager opts info = do
119 h <- HTTP.newManager (optHttpRPC opts)
120 u <- UDP.newManager (optUdpRPC opts) `onException` HTTP.closeManager h
121 return $ Manager opts info h u
122
123-- | Close all pending RPCs. Behaviour of currently in-flight RPCs can
124-- differ depending on underlying protocol used. No rpc calls should
125-- be performed after manager becomes closed.
126closeManager :: Manager -> IO ()
127closeManager Manager {..} = do
128 UDP.closeManager udpMgr `finally` HTTP.closeManager httpMgr
129
130-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
131withManager :: Options -> PeerInfo -> (Manager -> IO a) -> IO a
132withManager opts info = bracket (newManager opts info) closeManager
133
134{-----------------------------------------------------------------------
135-- Exceptions
136-----------------------------------------------------------------------}
137-- TODO Catch IO exceptions on rpc calls (?)
138
139data RpcException
140 = UdpException UDP.RpcException -- ^ UDP RPC driver failure;
141 | HttpException HTTP.RpcException -- ^ HTTP RPC driver failure;
142 | UnrecognizedScheme String -- ^ unsupported scheme in announce URI;
143 | GenericException String -- ^ for furter extensibility.
144 deriving (Show, Typeable)
145
146instance Exception RpcException
147
148packException :: Exception e => (e -> RpcException) -> IO a -> IO a
149packException f m = try m >>= either (throwIO . f) return
150{-# INLINE packException #-}
151
152{-----------------------------------------------------------------------
153-- RPC calls
154-----------------------------------------------------------------------}
155
156dispatch :: URI -> IO a -> IO a -> IO a
157dispatch URI {..} http udp
158 | uriScheme == "http:" ||
159 uriScheme == "https:" = packException HttpException http
160 | uriScheme == "udp:" = packException UdpException udp
161 | otherwise = throwIO $ UnrecognizedScheme uriScheme
162
163announce :: Manager -> URI -> SAnnounceQuery -> IO AnnounceInfo
164announce Manager {..} uri simpleQuery
165 = dispatch uri
166 (HTTP.announce httpMgr uri annQ)
167 ( UDP.announce udpMgr uri annQ)
168 where
169 annQ = fillAnnounceQuery peerInfo simpleQuery
170
171scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo
172scrape Manager {..} uri q
173 = dispatch uri
174 (HTTP.scrape httpMgr uri q)
175 ( UDP.scrape udpMgr uri q)
diff --git a/bittorrent/src/Network/BitTorrent/Tracker/RPC/HTTP.hs b/bittorrent/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
new file mode 100644
index 00000000..9b6e056a
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
@@ -0,0 +1,191 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : provisional
6-- Portability : portable
7--
8-- This module implement HTTP tracker protocol.
9--
10-- For more information see:
11-- <https://wiki.theory.org/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol>
12--
13{-# LANGUAGE DeriveDataTypeable #-}
14module Network.BitTorrent.Tracker.RPC.HTTP
15 ( -- * Manager
16 Options (..)
17 , Manager
18 , newManager
19 , closeManager
20 , withManager
21
22 -- * RPC
23 , RpcException (..)
24 , announce
25 , scrape
26 , scrapeOne
27 ) where
28
29import Control.Applicative
30import Control.Exception
31import Control.Monad
32import Control.Monad.Trans.Resource
33import Data.BEncode as BE
34import Data.ByteString as BS
35import Data.ByteString.Char8 as BC
36import Data.ByteString.Lazy as BL
37import Data.Default
38import Data.List as L
39import Data.Monoid
40import Data.Typeable hiding (Proxy)
41import Network.URI
42import Network.HTTP.Conduit hiding
43 (Manager, newManager, closeManager, withManager)
44import Network.HTTP.Client (defaultManagerSettings)
45import Network.HTTP.Client.Internal (setUri)
46import qualified Network.HTTP.Conduit as HTTP
47import Network.HTTP.Types.Header (hUserAgent)
48import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery)
49
50import Data.Torrent (InfoHash)
51import Network.Address (libUserAgent)
52import Network.BitTorrent.Tracker.Message hiding (Request, Response)
53
54{-----------------------------------------------------------------------
55-- Exceptions
56-----------------------------------------------------------------------}
57
58data RpcException
59 = RequestFailed HttpException -- ^ failed HTTP request.
60 | ParserFailure String -- ^ unable to decode tracker response;
61 | ScrapelessTracker -- ^ tracker do not support scraping;
62 | BadScrape -- ^ unable to find info hash in response dict;
63 deriving (Show, Typeable)
64
65instance Exception RpcException
66
67packHttpException :: IO a -> IO a
68packHttpException m = try m >>= either (throwIO . RequestFailed) return
69
70{-----------------------------------------------------------------------
71-- Manager
72-----------------------------------------------------------------------}
73
74-- | HTTP tracker specific RPC options.
75data Options = Options
76 { -- | Global HTTP announce query preferences.
77 optAnnouncePrefs :: !AnnouncePrefs
78
79 -- | Whether to use HTTP proxy for HTTP tracker requests.
80 , optHttpProxy :: !(Maybe Proxy)
81
82 -- | Value to put in HTTP user agent header.
83 , optUserAgent :: !BS.ByteString
84
85 -- | HTTP manager options.
86 , optHttpOptions :: !ManagerSettings
87 }
88
89instance Default Options where
90 def = Options
91 { optAnnouncePrefs = def
92 , optHttpProxy = Nothing
93 , optUserAgent = BC.pack libUserAgent
94 , optHttpOptions = defaultManagerSettings
95 }
96
97-- | HTTP tracker manager.
98data Manager = Manager
99 { options :: !Options
100 , httpMgr :: !HTTP.Manager
101 }
102
103-- |
104newManager :: Options -> IO Manager
105newManager opts = Manager opts <$> HTTP.newManager (optHttpOptions opts)
106
107-- |
108closeManager :: Manager -> IO ()
109closeManager Manager {..} = HTTP.closeManager httpMgr
110
111-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
112withManager :: Options -> (Manager -> IO a) -> IO a
113withManager opts = bracket (newManager opts) closeManager
114
115{-----------------------------------------------------------------------
116-- Queries
117-----------------------------------------------------------------------}
118
119fillRequest :: Options -> SimpleQuery -> Request -> Request
120fillRequest Options {..} q r = r
121 { queryString = joinQuery (queryString r) (renderSimpleQuery False q)
122 , requestHeaders = (hUserAgent, optUserAgent) : requestHeaders r
123 , proxy = optHttpProxy
124 }
125 where
126 joinQuery a b
127 | BS.null a = b
128 | otherwise = a <> "&" <> b
129
130httpTracker :: BEncode a => Manager -> URI -> SimpleQuery -> IO a
131httpTracker Manager {..} uri q = packHttpException $ do
132 request <- fillRequest options q <$> setUri def {- http-client instance for Request -} uri
133 response <- runResourceT $ httpLbs request httpMgr
134 case BE.decode $ BL.toStrict $ responseBody response of
135 Left msg -> throwIO (ParserFailure msg)
136 Right info -> return info
137
138{-----------------------------------------------------------------------
139-- RPC
140-----------------------------------------------------------------------}
141
142-- | Send request and receive response from the tracker specified in
143-- announce list.
144--
145-- This function can throw 'RpcException'.
146--
147announce :: Manager -> URI -> AnnounceQuery -> IO AnnounceInfo
148announce mgr uri q = httpTracker mgr uri (renderAnnounceRequest uriQ)
149 where
150 uriQ = AnnounceRequest
151 { announceQuery = q
152 , announcePrefs = optAnnouncePrefs (options mgr)
153 }
154
155-- | Trying to convert /announce/ URL to /scrape/ URL. If 'scrapeURL'
156-- gives 'Nothing' then tracker do not support scraping.
157--
158scrapeURL :: URI -> Maybe URI
159scrapeURL uri = do
160 newPath <- replace (BC.pack (uriPath uri))
161 return uri { uriPath = BC.unpack newPath }
162 where
163 replace p = do
164 let ps = BC.splitWith (== '/') p
165 guard (not (L.null ps))
166 guard ("announce" `BS.isPrefixOf` L.last ps)
167 let newSuff = "scrape" <> BS.drop (BS.length "announce") (L.last ps)
168 return (BS.intercalate "/" (L.init ps ++ [newSuff]))
169
170-- | For each 'InfoHash' of torrents request scrape info from the tracker.
171-- However if the info hash list is 'null', the tracker should list
172-- all available torrents.
173--
174-- This function can throw 'RpcException'.
175--
176scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo
177scrape m u q = do
178 case scrapeURL u of
179 Nothing -> throwIO ScrapelessTracker
180 Just uri -> httpTracker m uri (renderScrapeQuery q)
181
182-- | More particular version of 'scrape', just for one torrent.
183--
184-- This function can throw 'RpcException'.
185--
186scrapeOne :: Manager -> URI -> InfoHash -> IO ScrapeEntry
187scrapeOne m uri ih = do
188 xs <- scrape m uri [ih]
189 case L.lookup ih xs of
190 Nothing -> throwIO BadScrape
191 Just a -> return a
diff --git a/bittorrent/src/Network/BitTorrent/Tracker/RPC/UDP.hs b/bittorrent/src/Network/BitTorrent/Tracker/RPC/UDP.hs
new file mode 100644
index 00000000..31b6b870
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker/RPC/UDP.hs
@@ -0,0 +1,454 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013-2014
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : provisional
6-- Portability : portable
7--
8-- This module implement UDP tracker protocol.
9--
10-- For protocol details and uri scheme see:
11-- <http://www.bittorrent.org/beps/bep_0015.html>,
12-- <https://www.iana.org/assignments/uri-schemes/prov/udp>
13--
14{-# LANGUAGE RecordWildCards #-}
15{-# LANGUAGE FlexibleInstances #-}
16{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17{-# LANGUAGE DeriveDataTypeable #-}
18module Network.BitTorrent.Tracker.RPC.UDP
19 ( -- * Manager
20 Options (..)
21 , Manager
22 , newManager
23 , closeManager
24 , withManager
25
26 -- * RPC
27 , RpcException (..)
28 , announce
29 , scrape
30 ) where
31
32import Control.Applicative
33import Control.Concurrent
34import Control.Exception
35import Control.Monad
36import Data.Default
37import Data.IORef
38import Data.List as L
39import Data.Map as M
40import Data.Maybe
41import Data.Serialize
42import Data.Text as T
43import Data.Time
44import Data.Time.Clock.POSIX
45import Data.Traversable
46import Data.Typeable
47import Text.Read (readMaybe)
48import Network.Socket hiding (Connected, connect, listen)
49import Network.Socket.ByteString as BS
50import Network.URI
51import System.Timeout
52
53import Network.BitTorrent.Tracker.Message
54
55{-----------------------------------------------------------------------
56-- Options
57-----------------------------------------------------------------------}
58
59-- | 'System.Timeout.timeout' specific.
60sec :: Int
61sec = 1000000
62
63-- | See <http://www.bittorrent.org/beps/bep_0015.html#time-outs>
64defMinTimeout :: Int
65defMinTimeout = 15
66
67-- | See <http://www.bittorrent.org/beps/bep_0015.html#time-outs>
68defMaxTimeout :: Int
69defMaxTimeout = 15 * 2 ^ (8 :: Int)
70
71-- | See: <http://www.bittorrent.org/beps/bep_0015.html#time-outs>
72defMultiplier :: Int
73defMultiplier = 2
74
75-- TODO why 98?
76defMaxPacketSize :: Int
77defMaxPacketSize = 98
78
79-- | Manager configuration.
80data Options = Options
81 { -- | Max size of a /response/ packet.
82 --
83 -- 'optMaxPacketSize' /must/ be a positive value.
84 --
85 optMaxPacketSize :: {-# UNPACK #-} !Int
86
87 -- | Starting timeout interval in seconds. If a response is not
88 -- received after 'optMinTimeout' then 'Manager' repeat RPC with
89 -- timeout interval multiplied by 'optMultiplier' and so on until
90 -- timeout interval reach 'optMaxTimeout'.
91 --
92 -- 'optMinTimeout' /must/ be a positive value.
93 --
94 , optMinTimeout :: {-# UNPACK #-} !Int
95
96 -- | Final timeout interval in seconds. After 'optMaxTimeout'
97 -- reached and tracker still not responding both 'announce' and
98 -- 'scrape' functions will throw 'TimeoutExpired' exception.
99 --
100 -- 'optMaxTimeout' /must/ be greater than 'optMinTimeout'.
101 --
102 , optMaxTimeout :: {-# UNPACK #-} !Int
103
104 -- | 'optMultiplier' /must/ be a positive value.
105 , optMultiplier :: {-# UNPACK #-} !Int
106 } deriving (Show, Eq)
107
108-- | Options suitable for bittorrent client.
109instance Default Options where
110 def = Options
111 { optMaxPacketSize = defMaxPacketSize
112 , optMinTimeout = defMinTimeout
113 , optMaxTimeout = defMaxTimeout
114 , optMultiplier = defMultiplier
115 }
116
117checkOptions :: Options -> IO ()
118checkOptions Options {..} = do
119 unless (optMaxPacketSize > 0) $ do
120 throwIO $ userError "optMaxPacketSize must be positive"
121
122 unless (optMinTimeout > 0) $ do
123 throwIO $ userError "optMinTimeout must be positive"
124
125 unless (optMaxTimeout > 0) $ do
126 throwIO $ userError "optMaxTimeout must be positive"
127
128 unless (optMultiplier > 0) $ do
129 throwIO $ userError "optMultiplier must be positive"
130
131 unless (optMaxTimeout > optMinTimeout) $ do
132 throwIO $ userError "optMaxTimeout must be greater than optMinTimeout"
133
134
135{-----------------------------------------------------------------------
136-- Manager state
137-----------------------------------------------------------------------}
138
139type ConnectionCache = Map SockAddr Connection
140
141type PendingResponse = MVar (Either RpcException Response)
142type PendingTransactions = Map TransactionId PendingResponse
143type PendingQueries = Map SockAddr PendingTransactions
144
145-- | UDP tracker manager.
146data Manager = Manager
147 { options :: !Options
148 , sock :: !Socket
149-- , dnsCache :: !(IORef (Map URI SockAddr))
150 , connectionCache :: !(IORef ConnectionCache)
151 , pendingResps :: !(MVar PendingQueries)
152 , listenerThread :: !(MVar ThreadId)
153 }
154
155initManager :: Options -> IO Manager
156initManager opts = Manager opts
157 <$> socket AF_INET Datagram defaultProtocol
158 <*> newIORef M.empty
159 <*> newMVar M.empty
160 <*> newEmptyMVar
161
162unblockAll :: PendingQueries -> IO ()
163unblockAll m = traverse (traverse unblockCall) m >> return ()
164 where
165 unblockCall ares = putMVar ares (Left ManagerClosed)
166
167resetState :: Manager -> IO ()
168resetState Manager {..} = do
169 writeIORef connectionCache err
170 m <- swapMVar pendingResps err
171 unblockAll m
172 mtid <- tryTakeMVar listenerThread
173 case mtid of
174 Nothing -> return () -- thread killed by 'closeManager'
175 Just _ -> return () -- thread killed by exception from 'listen'
176 return ()
177 where
178 err = error "UDP tracker manager closed"
179
180-- | This function will throw 'IOException' on invalid 'Options'.
181newManager :: Options -> IO Manager
182newManager opts = do
183 checkOptions opts
184 mgr <- initManager opts
185 tid <- forkIO (listen mgr `finally` resetState mgr)
186 putMVar (listenerThread mgr) tid
187 return mgr
188
189-- | Unblock all RPCs by throwing 'ManagerClosed' exception. No rpc
190-- calls should be performed after manager becomes closed.
191closeManager :: Manager -> IO ()
192closeManager Manager {..} = do
193 close sock
194 mtid <- tryTakeMVar listenerThread
195 case mtid of
196 Nothing -> return ()
197 Just tid -> killThread tid
198
199-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
200withManager :: Options -> (Manager -> IO a) -> IO a
201withManager opts = bracket (newManager opts) closeManager
202
203{-----------------------------------------------------------------------
204-- Exceptions
205-----------------------------------------------------------------------}
206
207data RpcException
208 -- | Unable to lookup hostname;
209 = HostUnknown
210
211 -- | Unable to lookup hostname;
212 | HostLookupFailed
213
214 -- | Expecting 'udp:', but some other scheme provided.
215 | UnrecognizedScheme String
216
217 -- | Tracker exists but not responding for specific number of seconds.
218 | TimeoutExpired Int
219
220 -- | Tracker responded with unexpected message type.
221 | UnexpectedResponse
222 { expectedMsg :: String
223 , actualMsg :: String
224 }
225
226 -- | RPC succeed, but tracker responded with error code.
227 | QueryFailed Text
228
229 -- | RPC manager closed while waiting for response.
230 | ManagerClosed
231 deriving (Eq, Show, Typeable)
232
233instance Exception RpcException
234
235{-----------------------------------------------------------------------
236-- Host Addr resolution
237-----------------------------------------------------------------------}
238
239setPort :: PortNumber -> SockAddr -> SockAddr
240setPort p (SockAddrInet _ h) = SockAddrInet p h
241setPort p (SockAddrInet6 _ f h s) = SockAddrInet6 p f h s
242setPort _ addr = addr
243
244resolveURI :: URI -> IO SockAddr
245resolveURI URI { uriAuthority = Just (URIAuth {..}) } = do
246 infos <- getAddrInfo Nothing (Just uriRegName) Nothing
247 let port = fromMaybe 0 (readMaybe (L.drop 1 uriPort) :: Maybe Int)
248 case infos of
249 AddrInfo {..} : _ -> return $ setPort (fromIntegral port) addrAddress
250 _ -> throwIO HostLookupFailed
251resolveURI _ = throwIO HostUnknown
252
253-- TODO caching?
254getTrackerAddr :: Manager -> URI -> IO SockAddr
255getTrackerAddr _ uri
256 | uriScheme uri == "udp:" = resolveURI uri
257 | otherwise = throwIO (UnrecognizedScheme (uriScheme uri))
258
259{-----------------------------------------------------------------------
260 Connection
261-----------------------------------------------------------------------}
262
263connectionLifetime :: NominalDiffTime
264connectionLifetime = 60
265
266data Connection = Connection
267 { connectionId :: ConnectionId
268 , connectionTimestamp :: UTCTime
269 } deriving Show
270
271-- placeholder for the first 'connect'
272initialConnection :: Connection
273initialConnection = Connection initialConnectionId (posixSecondsToUTCTime 0)
274
275establishedConnection :: ConnectionId -> IO Connection
276establishedConnection cid = Connection cid <$> getCurrentTime
277
278isExpired :: Connection -> IO Bool
279isExpired Connection {..} = do
280 currentTime <- getCurrentTime
281 let timeDiff = diffUTCTime currentTime connectionTimestamp
282 return $ timeDiff > connectionLifetime
283
284{-----------------------------------------------------------------------
285-- Transactions
286-----------------------------------------------------------------------}
287
288-- | Sometimes 'genTransactionId' may return already used transaction
289-- id. We use a good entropy source but the issue /still/ (with very
290-- small probabality) may happen. If the collision happen then this
291-- function tries to find nearest unused slot, otherwise pending
292-- transactions table is full.
293firstUnused :: SockAddr -> TransactionId -> PendingQueries -> TransactionId
294firstUnused addr rid m = do
295 case M.splitLookup rid <$> M.lookup addr m of
296 Nothing -> rid
297 Just (_ , Nothing, _ ) -> rid
298 Just (lt, Just _ , gt) ->
299 case backwardHole (keys lt) rid <|> forwardHole rid (keys gt) of
300 Nothing -> error "firstUnused: table is full" -- impossible
301 Just tid -> tid
302 where
303 forwardHole a []
304 | a == maxBound = Nothing
305 | otherwise = Just (succ a)
306 forwardHole a (b : xs)
307 | succ a == b = forwardHole b xs
308 | otherwise = Just (succ a)
309
310 backwardHole [] a
311 | a == minBound = Nothing
312 | otherwise = Just (pred a)
313 backwardHole (b : xs) a
314 | b == pred a = backwardHole xs b
315 | otherwise = Just (pred a)
316
317register :: SockAddr -> TransactionId -> PendingResponse
318 -> PendingQueries -> PendingQueries
319register addr tid ares = M.alter insertId addr
320 where
321 insertId Nothing = Just (M.singleton tid ares)
322 insertId (Just m) = Just (M.insert tid ares m)
323
324unregister :: SockAddr -> TransactionId
325 -> PendingQueries -> PendingQueries
326unregister addr tid = M.update deleteId addr
327 where
328 deleteId m
329 | M.null m' = Nothing
330 | otherwise = Just m'
331 where
332 m' = M.delete tid m
333
334-- | Generate a new unused transaction id and register as pending.
335allocTransaction :: Manager -> SockAddr -> PendingResponse -> IO TransactionId
336allocTransaction Manager {..} addr ares =
337 modifyMVar pendingResps $ \ m -> do
338 rndId <- genTransactionId
339 let tid = firstUnused addr rndId m
340 return (register addr tid ares m, tid)
341
342-- | Wake up blocked thread and return response back.
343commitTransaction :: Manager -> SockAddr -> TransactionId -> Response -> IO ()
344commitTransaction Manager {..} addr tid resp =
345 modifyMVarMasked_ pendingResps $ \ m -> do
346 case M.lookup tid =<< M.lookup addr m of
347 Nothing -> return m -- tracker responded after 'cancelTransaction' fired
348 Just ares -> do
349 putMVar ares (Right resp)
350 return $ unregister addr tid m
351
352-- | Abort transaction forcefully.
353cancelTransaction :: Manager -> SockAddr -> TransactionId -> IO ()
354cancelTransaction Manager {..} addr tid =
355 modifyMVarMasked_ pendingResps $ \m ->
356 return $ unregister addr tid m
357
358-- | Handle responses from trackers.
359listen :: Manager -> IO ()
360listen mgr @ Manager {..} = do
361 forever $ do
362 (bs, addr) <- BS.recvFrom sock (optMaxPacketSize options)
363 case decode bs of
364 Left _ -> return () -- parser failed, ignoring
365 Right (TransactionR {..}) -> commitTransaction mgr addr transIdR response
366
367-- | Perform RPC transaction. If the action interrupted transaction
368-- will be aborted.
369transaction :: Manager -> SockAddr -> Connection -> Request -> IO Response
370transaction mgr @ Manager {..} addr conn request = do
371 ares <- newEmptyMVar
372 tid <- allocTransaction mgr addr ares
373 performTransaction tid ares
374 `onException` cancelTransaction mgr addr tid
375 where
376 performTransaction tid ares = do
377 let trans = TransactionQ (connectionId conn) tid request
378 BS.sendAllTo sock (encode trans) addr
379 takeMVar ares >>= either throwIO return
380
381{-----------------------------------------------------------------------
382-- Connection cache
383-----------------------------------------------------------------------}
384
385connect :: Manager -> SockAddr -> Connection -> IO ConnectionId
386connect m addr conn = do
387 resp <- transaction m addr conn Connect
388 case resp of
389 Connected cid -> return cid
390 Failed msg -> throwIO $ QueryFailed msg
391 _ -> throwIO $ UnexpectedResponse "connected" (responseName resp)
392
393newConnection :: Manager -> SockAddr -> IO Connection
394newConnection m addr = do
395 connId <- connect m addr initialConnection
396 establishedConnection connId
397
398refreshConnection :: Manager -> SockAddr -> Connection -> IO Connection
399refreshConnection mgr addr conn = do
400 expired <- isExpired conn
401 if expired
402 then do
403 connId <- connect mgr addr conn
404 establishedConnection connId
405 else do
406 return conn
407
408withCache :: Manager -> SockAddr
409 -> (Maybe Connection -> IO Connection) -> IO Connection
410withCache mgr addr action = do
411 cache <- readIORef (connectionCache mgr)
412 conn <- action (M.lookup addr cache)
413 writeIORef (connectionCache mgr) (M.insert addr conn cache)
414 return conn
415
416getConnection :: Manager -> SockAddr -> IO Connection
417getConnection mgr addr = withCache mgr addr $
418 maybe (newConnection mgr addr) (refreshConnection mgr addr)
419
420{-----------------------------------------------------------------------
421-- RPC
422-----------------------------------------------------------------------}
423
424retransmission :: Options -> IO a -> IO a
425retransmission Options {..} action = go optMinTimeout
426 where
427 go curTimeout
428 | curTimeout > optMaxTimeout = throwIO $ TimeoutExpired curTimeout
429 | otherwise = do
430 r <- timeout (curTimeout * sec) action
431 maybe (go (optMultiplier * curTimeout)) return r
432
433queryTracker :: Manager -> URI -> Request -> IO Response
434queryTracker mgr uri req = do
435 addr <- getTrackerAddr mgr uri
436 retransmission (options mgr) $ do
437 conn <- getConnection mgr addr
438 transaction mgr addr conn req
439
440-- | This function can throw 'RpcException'.
441announce :: Manager -> URI -> AnnounceQuery -> IO AnnounceInfo
442announce mgr uri q = do
443 resp <- queryTracker mgr uri (Announce q)
444 case resp of
445 Announced info -> return info
446 _ -> throwIO $ UnexpectedResponse "announce" (responseName resp)
447
448-- | This function can throw 'RpcException'.
449scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo
450scrape mgr uri ihs = do
451 resp <- queryTracker mgr uri (Scrape ihs)
452 case resp of
453 Scraped info -> return $ L.zip ihs info
454 _ -> throwIO $ UnexpectedResponse "scrape" (responseName resp)
diff --git a/bittorrent/src/Network/BitTorrent/Tracker/Session.hs b/bittorrent/src/Network/BitTorrent/Tracker/Session.hs
new file mode 100644
index 00000000..aa4a832f
--- /dev/null
+++ b/bittorrent/src/Network/BitTorrent/Tracker/Session.hs
@@ -0,0 +1,306 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2014
3-- License : BSD
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Multitracker sessions.
9--
10{-# LANGUAGE FlexibleInstances #-}
11{-# LANGUAGE TypeFamilies #-}
12{-# LANGUAGE TypeSynonymInstances #-}
13{-# LANGUAGE TemplateHaskell #-}
14module Network.BitTorrent.Tracker.Session
15 ( -- * Session
16 Session
17 , Event (..)
18 , newSession
19 , closeSession
20 , withSession
21
22 -- * Client send notifications
23 , notify
24 , askPeers
25
26 -- * Session state
27 -- ** Status
28 , Status (..)
29 , getStatus
30
31 -- ** Single tracker sessions
32 , LastScrape (..)
33 , TrackerSession
34 , trackerPeers
35 , trackerScrape
36 , getSessionState
37
38 -- * Tracker Exchange
39 -- | BEP28: <http://www.bittorrent.org/beps/bep_0028.html>
40 , addTracker
41 , removeTracker
42 , getTrustedTrackers
43 ) where
44
45import Control.Applicative
46import Control.Exception
47import Control.Concurrent
48import Control.Concurrent.Chan.Split as CS
49import Control.Monad
50import Data.Default
51import Data.Fixed
52import Data.Foldable as F
53import Data.IORef
54import Data.List as L
55import Data.Maybe
56import Data.Time
57import Data.Traversable
58import Network.URI
59
60import Data.Torrent
61import Network.Address
62import Network.BitTorrent.Internal.Cache
63import Network.BitTorrent.Internal.Types
64import Network.BitTorrent.Tracker.List as TL
65import Network.BitTorrent.Tracker.Message
66import Network.BitTorrent.Tracker.RPC as RPC
67
68{-----------------------------------------------------------------------
69-- Single tracker session
70-----------------------------------------------------------------------}
71
72-- | Status of this client.
73data Status
74 = Running -- ^ This client is announced and listenning for incoming
75 -- connections.
76 | Paused -- ^ This client does not expecting incoming connections.
77 deriving (Show, Eq, Bounded, Enum)
78
79-- | Client starting in the paused state.
80instance Default Status where
81 def = Paused
82
83-- | Tracker session starts with scrape unknown.
84instance Default LastScrape where
85 def = LastScrape Nothing Nothing
86
87data LastScrape = LastScrape
88 { -- | Count of leechers the tracker aware of.
89 scrapeLeechers :: Maybe Int
90
91 -- | Count of seeders the tracker aware of.
92 , scrapeSeeders :: Maybe Int
93 } deriving (Show, Eq)
94
95-- | Single tracker session.
96data TrackerSession = TrackerSession
97 { -- | Used to notify 'Stopped' and 'Completed' events.
98 statusSent :: !(Maybe Status)
99
100 -- | Can be used to retrieve peer set.
101 , trackerPeers :: Cached [PeerAddr IP]
102
103 -- | Can be used to show brief swarm stats in client GUI.
104 , trackerScrape :: Cached LastScrape
105 }
106
107-- | Not contacted.
108instance Default TrackerSession where
109 def = TrackerSession Nothing def def
110
111-- | Do we need to notify this /specific/ tracker?
112needNotify :: AnnounceEvent -> Maybe Status -> Maybe Bool
113needNotify Started Nothing = Just True
114needNotify Stopped Nothing = Just False
115needNotify Completed Nothing = Just False
116needNotify Started (Just Running) = Nothing
117needNotify Stopped (Just Running) = Just True
118needNotify Completed (Just Running) = Just True
119needNotify Started (Just Paused ) = Just True
120needNotify Stopped (Just Paused ) = Just False
121needNotify Completed (Just Paused ) = Just True
122
123-- | Client status after event announce succeed.
124nextStatus :: AnnounceEvent -> Maybe Status
125nextStatus Started = Just Running
126nextStatus Stopped = Just Paused
127nextStatus Completed = Nothing -- must keep previous status
128
129seconds :: Int -> NominalDiffTime
130seconds n = realToFrac (toEnum n :: Uni)
131
132cachePeers :: AnnounceInfo -> IO (Cached [PeerAddr IP])
133cachePeers AnnounceInfo {..} =
134 newCached (seconds respInterval)
135 (seconds (fromMaybe respInterval respMinInterval))
136 (getPeerList respPeers)
137
138cacheScrape :: AnnounceInfo -> IO (Cached LastScrape)
139cacheScrape AnnounceInfo {..} =
140 newCached (seconds respInterval)
141 (seconds (fromMaybe respInterval respMinInterval))
142 LastScrape
143 { scrapeSeeders = respComplete
144 , scrapeLeechers = respIncomplete
145 }
146
147-- | Make announce request to specific tracker returning new state.
148notifyTo :: Manager -> Session -> AnnounceEvent
149 -> TierEntry TrackerSession -> IO TrackerSession
150notifyTo mgr s @ Session {..} event (uri, entry @ TrackerSession {..}) = do
151 let shouldNotify = needNotify event statusSent
152 mustNotify <- maybe (isExpired trackerPeers) return shouldNotify
153 if not mustNotify
154 then return entry
155 else do
156 let q = SAnnounceQuery sessionTopic def Nothing (Just event)
157 res <- RPC.announce mgr uri q
158 when (statusSent == Nothing) $ do
159 send sessionEvents (TrackerConfirmed uri)
160 send sessionEvents (AnnouncedTo uri)
161 let status' = nextStatus event <|> statusSent
162 TrackerSession status' <$> cachePeers res <*> cacheScrape res
163
164{-----------------------------------------------------------------------
165-- Multitracker Session
166-----------------------------------------------------------------------}
167
168-- | Multitracker session.
169data Session = Session
170 { -- | Infohash to announce at each 'announce' request.
171 sessionTopic :: !InfoHash
172
173 -- | Current status of this client is used to filter duplicated
174 -- notifications, for e.g. we don't want to notify a tracker with
175 -- ['Stopped', 'Stopped'], the last should be ignored.
176 , sessionStatus :: !(IORef Status)
177
178 -- | A set of single-tracker sessions. Any request to a tracker
179 -- must take a lock.
180 , sessionTrackers :: !(MVar (TrackerList TrackerSession))
181
182 , sessionEvents :: !(SendPort (Event Session))
183 }
184
185instance EventSource Session where
186 data Event Session
187 = TrackerAdded URI
188 | TrackerConfirmed URI
189 | TrackerRemoved URI
190 | AnnouncedTo URI
191 | SessionClosed
192
193 listen Session {..} = CS.listen sessionEvents
194
195
196-- | Create a new multitracker session in paused state. Tracker list
197-- must contant only /trusted/ tracker uris. To start announcing
198-- client presence use 'notify'.
199newSession :: InfoHash -> TrackerList () -> IO Session
200newSession ih origUris = do
201 urisList <- shuffleTiers origUris
202 statusRef <- newIORef def
203 entriesVar <- newMVar (fmap (const def) urisList)
204 eventStream <- newSendPort
205 return Session
206 { sessionTopic = ih
207 , sessionStatus = statusRef
208 , sessionTrackers = entriesVar
209 , sessionEvents = eventStream
210 }
211
212-- | Release scarce resources associated with the given session. This
213-- function block until all trackers tied with this peer notified with
214-- 'Stopped' event.
215closeSession :: Manager -> Session -> IO ()
216closeSession m s @ Session {..} = do
217 notify m s Stopped
218 send sessionEvents SessionClosed
219
220{-----------------------------------------------------------------------
221-- Operations
222-----------------------------------------------------------------------}
223
224-- | Normally you need to use 'Control.Monad.Trans.Resource.alloc'.
225withSession :: Manager -> InfoHash -> TrackerList ()
226 -> (Session -> IO ()) -> IO ()
227withSession m ih uris = bracket (newSession ih uris) (closeSession m)
228
229-- | Get last announced status. The only action can alter this status
230-- is 'notify'.
231getStatus :: Session -> IO Status
232getStatus Session {..} = readIORef sessionStatus
233
234getSessionState :: Session -> IO [[TierEntry TrackerSession]]
235getSessionState Session {..} = TL.toList <$> readMVar sessionTrackers
236
237-- | Do we need to sent this event to a first working tracker or to
238-- the all known good trackers?
239allNotify :: AnnounceEvent -> Bool
240allNotify Started = False
241allNotify Stopped = True
242allNotify Completed = True
243
244notifyAll :: Manager -> Session -> AnnounceEvent -> IO ()
245notifyAll mgr s @ Session {..} event = do
246 modifyMVar_ sessionTrackers $
247 (traversal (notifyTo mgr s event))
248 where
249 traversal
250 | allNotify event = traverseAll
251 | otherwise = traverseTiers
252
253-- TODO send notifications to tracker periodically.
254-- |
255--
256-- This function /may/ block until tracker query proceed.
257notify :: Manager -> Session -> AnnounceEvent -> IO ()
258notify mgr ses event = do
259 prevStatus <- atomicModifyIORef (sessionStatus ses) $ \ s ->
260 (fromMaybe s (nextStatus event), s)
261 when (needNotify event (Just prevStatus) == Just True) $ do
262 notifyAll mgr ses event
263
264-- TODO run announce if sesion have no peers
265-- | The returned list of peers can have duplicates.
266-- This function /may/ block. Use async if needed.
267askPeers :: Manager -> Session -> IO [PeerAddr IP]
268askPeers _mgr ses = do
269 list <- readMVar (sessionTrackers ses)
270 L.concat <$> collect (tryTakeData . trackerPeers) list
271
272collect :: (a -> IO (Maybe b)) -> TrackerList a -> IO [b]
273collect f lst = (catMaybes . F.toList) <$> traverse f lst
274
275--sourcePeers :: Session -> Source (PeerAddr IP)
276--sourcePeers
277
278{-----------------------------------------------------------------------
279-- Tracker exchange
280-----------------------------------------------------------------------}
281
282-- Trackers discovered through this protocol SHOULD be treated with a
283-- certain amount of suspicion. Since the source of a tracker exchange
284-- message cannot be trusted, an implementation SHOULD have a lower
285-- number of retries before giving up entirely.
286
287addTracker :: Session -> URI -> IO ()
288addTracker Session {..} uri = do
289 undefined
290 send sessionEvents (TrackerAdded uri)
291
292removeTracker :: Manager -> Session -> URI -> IO ()
293removeTracker m Session {..} uri = do
294 send sessionEvents (TrackerRemoved uri)
295
296-- Also, as specified under the definitions section, a tracker that
297-- has not worked should never be propagated to other peers over the
298-- tracker exchange protocol.
299
300-- | Return all known trackers.
301getTrackers :: Session -> IO [URI]
302getTrackers = undefined
303
304-- | Return trackers from torrent file and
305getTrustedTrackers :: Session -> IO [URI]
306getTrustedTrackers = undefined
diff --git a/bittorrent/src/System/Torrent/FileMap.hs b/bittorrent/src/System/Torrent/FileMap.hs
new file mode 100644
index 00000000..6e8d7f5a
--- /dev/null
+++ b/bittorrent/src/System/Torrent/FileMap.hs
@@ -0,0 +1,151 @@
1{-# LANGUAGE RecordWildCards #-}
2{-# LANGUAGE ViewPatterns #-}
3{-# OPTIONS -fno-warn-orphans #-}
4module System.Torrent.FileMap
5 ( FileMap
6
7 -- * Construction
8 , Mode (..)
9 , def
10 , mmapFiles
11 , unmapFiles
12
13 -- * Query
14 , System.Torrent.FileMap.size
15
16 -- * Modification
17 , readBytes
18 , writeBytes
19 , unsafeReadBytes
20
21 -- * Unsafe conversions
22 , fromLazyByteString
23 , toLazyByteString
24 ) where
25
26import Control.Applicative
27import Control.Monad as L
28import Data.ByteString as BS
29import Data.ByteString.Internal as BS
30import Data.ByteString.Lazy as BL
31import Data.ByteString.Lazy.Internal as BL
32import Data.Default
33import Data.Vector as V -- TODO use unboxed vector
34import Foreign
35import System.IO.MMap
36
37import Data.Torrent
38
39
40data FileEntry = FileEntry
41 { filePosition :: {-# UNPACK #-} !FileOffset
42 , fileBytes :: {-# UNPACK #-} !BS.ByteString
43 } deriving (Show, Eq)
44
45type FileMap = Vector FileEntry
46
47instance Default Mode where
48 def = ReadWriteEx
49
50mmapFiles :: Mode -> FileLayout FileSize -> IO FileMap
51mmapFiles mode layout = V.fromList <$> L.mapM mkEntry (accumPositions layout)
52 where
53 mkEntry (path, (pos, expectedSize)) = do
54 let esize = fromIntegral expectedSize -- FIXME does this safe?
55 (fptr, moff, msize) <- mmapFileForeignPtr path mode $ Just (0, esize)
56 if msize /= esize
57 then error "mmapFiles" -- TODO unmap mapped files on exception
58 else return $ FileEntry pos (PS fptr moff msize)
59
60unmapFiles :: FileMap -> IO ()
61unmapFiles = V.mapM_ unmapEntry
62 where
63 unmapEntry (FileEntry _ (PS fptr _ _)) = finalizeForeignPtr fptr
64
65fromLazyByteString :: BL.ByteString -> FileMap
66fromLazyByteString lbs = V.unfoldr f (0, lbs)
67 where
68 f (_, Empty ) = Nothing
69 f (pos, Chunk x xs) = Just (FileEntry pos x, ((pos + chunkSize), xs))
70 where chunkSize = fromIntegral $ BS.length x
71
72-- | /O(n)/.
73toLazyByteString :: FileMap -> BL.ByteString
74toLazyByteString = V.foldr f Empty
75 where
76 f FileEntry {..} bs = Chunk fileBytes bs
77
78-- | /O(1)/.
79size :: FileMap -> FileOffset
80size m
81 | V.null m = 0
82 | FileEntry {..} <- V.unsafeLast m
83 = filePosition + fromIntegral (BS.length fileBytes)
84
85bsearch :: FileOffset -> FileMap -> Maybe Int
86bsearch x m
87 | V.null m = Nothing
88 | otherwise = branch (V.length m `div` 2)
89 where
90 branch c @ ((m !) -> FileEntry {..})
91 | x < filePosition = bsearch x (V.take c m)
92 | x >= filePosition + fileSize = do
93 ix <- bsearch x (V.drop (succ c) m)
94 return $ succ c + ix
95 | otherwise = Just c
96 where
97 fileSize = fromIntegral (BS.length fileBytes)
98
99-- | /O(log n)/.
100drop :: FileOffset -> FileMap -> (FileSize, FileMap)
101drop off m
102 | Just ix <- bsearch off m
103 , FileEntry {..} <- m ! ix = (off - filePosition, V.drop ix m)
104 | otherwise = (0 , V.empty)
105
106-- | /O(log n)/.
107take :: FileSize -> FileMap -> (FileMap, FileSize)
108take len m
109 | len >= s = (m , 0)
110 | Just ix <- bsearch (pred len) m = let m' = V.take (succ ix) m
111 in (m', System.Torrent.FileMap.size m' - len)
112 | otherwise = (V.empty , 0)
113 where
114 s = System.Torrent.FileMap.size m
115
116-- | /O(log n + m)/. Do not use this function with 'unmapFiles'.
117unsafeReadBytes :: FileOffset -> FileSize -> FileMap -> BL.ByteString
118unsafeReadBytes off s m
119 | (l , m') <- System.Torrent.FileMap.drop off m
120 , (m'', _ ) <- System.Torrent.FileMap.take (off + s) m'
121 = BL.take (fromIntegral s) $ BL.drop (fromIntegral l) $ toLazyByteString m''
122
123readBytes :: FileOffset -> FileSize -> FileMap -> IO BL.ByteString
124readBytes off s m = do
125 let bs_copy = BL.copy $ unsafeReadBytes off s m
126 forceLBS bs_copy
127 return bs_copy
128 where
129 forceLBS Empty = return ()
130 forceLBS (Chunk _ x) = forceLBS x
131
132bscpy :: BL.ByteString -> BL.ByteString -> IO ()
133bscpy (PS _ _ 0 `Chunk` dest_rest) src = bscpy dest_rest src
134bscpy dest (PS _ _ 0 `Chunk` src_rest) = bscpy dest src_rest
135bscpy (PS dest_fptr dest_off dest_size `Chunk` dest_rest)
136 (PS src_fptr src_off src_size `Chunk` src_rest)
137 = do let csize = min dest_size src_size
138 withForeignPtr dest_fptr $ \dest_ptr ->
139 withForeignPtr src_fptr $ \src_ptr ->
140 memcpy (dest_ptr `advancePtr` dest_off)
141 (src_ptr `advancePtr` src_off)
142 (fromIntegral csize) -- TODO memmove?
143 bscpy (PS dest_fptr (dest_off + csize) (dest_size - csize) `Chunk` dest_rest)
144 (PS src_fptr (src_off + csize) (src_size - csize) `Chunk` src_rest)
145bscpy _ _ = return ()
146
147writeBytes :: FileOffset -> BL.ByteString -> FileMap -> IO ()
148writeBytes off lbs m = bscpy dest src
149 where
150 src = BL.take (fromIntegral (BL.length dest)) lbs
151 dest = unsafeReadBytes off (fromIntegral (BL.length lbs)) m \ No newline at end of file
diff --git a/bittorrent/src/System/Torrent/Storage.hs b/bittorrent/src/System/Torrent/Storage.hs
new file mode 100644
index 00000000..1d77e55d
--- /dev/null
+++ b/bittorrent/src/System/Torrent/Storage.hs
@@ -0,0 +1,221 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- This module implements mapping from single continious piece space
9-- to file storage. Storage can be used in two modes:
10--
11-- * As in memory storage - in this case we don't touch filesystem.
12--
13-- * As ordinary mmaped file storage - when we need to store
14-- data in the filesystem.
15--
16{-# LANGUAGE RecordWildCards #-}
17{-# LANGUAGE DeriveDataTypeable #-}
18module System.Torrent.Storage
19 ( -- * Storage
20 Storage
21 , StorageFailure (..)
22
23 -- * Construction
24 , Mode (..)
25 , def
26 , open
27 , openInfoDict
28 , close
29 , withStorage
30
31 -- * Query
32 , totalPieces
33 , verifyPiece
34 , genPieceInfo
35 , getBitfield
36
37 -- * Modification
38 , writePiece
39 , readPiece
40 , hintRead
41 , unsafeReadPiece
42
43 -- * Streaming
44 , sourceStorage
45 , sinkStorage
46 ) where
47
48import Control.Applicative
49import Control.Exception
50import Control.Monad as M
51import Control.Monad.Trans
52import Data.ByteString.Lazy as BL
53import Data.Conduit as C
54import Data.Conduit.Binary as C
55import Data.Conduit.List as C
56import Data.Typeable
57
58import Data.Torrent
59import Network.BitTorrent.Exchange.Bitfield as BF
60import System.Torrent.FileMap as FM
61
62
63-- | Some storage operations may throw an exception if misused.
64data StorageFailure
65 -- | Occurs on a write operation if the storage has been opened
66 -- using 'ReadOnly' mode.
67 = StorageIsRO
68
69 -- | Piece index is out of bounds.
70 | InvalidIndex PieceIx
71
72 -- | Piece size do not match with one passed to the 'open'
73 -- function.
74 | InvalidSize PieceSize
75 deriving (Show, Eq, Typeable)
76
77instance Exception StorageFailure
78
79-- | Pieces store.
80data Storage = Storage
81 { mode :: !Mode
82 , pieceLen :: {-# UNPACK #-} !PieceSize
83 , fileMap :: {-# UNPACK #-} !FileMap
84 }
85
86-- | Map torrent files:
87--
88-- * when torrent first created use 'ReadWriteEx' mode;
89--
90-- * when seeding, validation 'ReadOnly' mode.
91--
92open :: Mode -> PieceSize -> FileLayout FileSize -> IO Storage
93open mode s l
94 | s <= 0 = throwIO (InvalidSize s)
95 | otherwise = Storage mode s <$> mmapFiles mode l
96
97-- | Like 'open', but use 'InfoDict' file layout.
98openInfoDict :: Mode -> FilePath -> InfoDict -> IO Storage
99openInfoDict mode rootPath InfoDict {..} =
100 open mode (piPieceLength idPieceInfo) (flatLayout rootPath idLayoutInfo)
101
102-- | Unmaps all files forcefully. It is recommended but not required.
103close :: Storage -> IO ()
104close Storage {..} = unmapFiles fileMap
105
106-- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'.
107withStorage :: Mode -> PieceSize -> FileLayout FileSize
108 -> (Storage -> IO ()) -> IO ()
109withStorage m s l = bracket (open m s l) close
110
111-- TODO allocateStorage?
112
113-- | Count of pieces in the storage.
114totalPieces :: Storage -> PieceCount
115totalPieces Storage {..} = FM.size fileMap `sizeInBase` pieceLen
116
117isValidIx :: PieceIx -> Storage -> Bool
118isValidIx i s = 0 <= i && i < totalPieces s
119
120-- | Put piece data at the piece index by overwriting existing
121-- data.
122--
123-- This operation may throw 'StorageFailure'.
124--
125writePiece :: Piece BL.ByteString -> Storage -> IO ()
126writePiece p @ Piece {..} s @ Storage {..}
127 | mode == ReadOnly = throwIO StorageIsRO
128 | isNotValidIx pieceIndex = throwIO (InvalidIndex pieceIndex)
129 | isNotValidSize pieceIndex (pieceSize p)
130 = throwIO (InvalidSize (pieceSize p))
131 | otherwise = writeBytes offset pieceData fileMap
132 where
133 isNotValidSize pix psize
134 | succ pix == pcount = psize /= lastPieceLen -- last piece may be shorter
135 | otherwise = psize /= pieceLen
136 where
137 lastPieceLen = fromIntegral (FM.size fileMap `rem` fromIntegral pieceLen)
138 {-# INLINE isNotValidSize #-}
139
140 isNotValidIx i = i < 0 || i >= pcount
141 {-# INLINE isNotValidIx #-}
142
143 pcount = totalPieces s
144 offset = fromIntegral pieceIndex * fromIntegral pieceLen
145
146-- | Read specific piece from storage.
147--
148-- This operation may throw 'StorageFailure'.
149--
150readPiece :: PieceIx -> Storage -> IO (Piece BL.ByteString)
151readPiece pix s @ Storage {..}
152 | not (isValidIx pix s) = throwIO (InvalidIndex pix)
153 | otherwise = Piece pix <$> readBytes offset sz fileMap
154 where
155 offset = fromIntegral pix * fromIntegral pieceLen
156 sz = fromIntegral pieceLen
157
158-- | Hint about the coming 'readPiece'. Ignores invalid indexes, for e.g.:
159--
160-- @forall s. hindRead (-1) s == return ()@
161--
162hintRead :: PieceIx -> Storage -> IO ()
163hintRead _pix Storage {..} = return ()
164
165-- | Zero-copy version of readPiece. Can be used only with 'ReadOnly'
166-- storages.
167unsafeReadPiece :: PieceIx -> Storage -> IO (Piece BL.ByteString)
168unsafeReadPiece pix s @ Storage {..}
169 | not (isValidIx pix s) = throwIO (InvalidIndex pix)
170 | otherwise = return $ Piece pix (unsafeReadBytes offset sz fileMap)
171 where
172 offset = fromIntegral pix * fromIntegral pieceLen
173 sz = fromIntegral pieceLen
174
175-- | Stream storage pieces from first to the last.
176sourceStorage :: Storage -> Source IO (Piece BL.ByteString)
177sourceStorage s = go 0
178 where
179 go pix
180 | pix < totalPieces s = do
181 piece <- liftIO $ readPiece pix s
182 liftIO $ hintRead (succ pix) s
183 yield piece
184 go (succ pix)
185 | otherwise = return ()
186
187-- | Write stream of pieces to the storage. Fail if storage is 'ReadOnly'.
188sinkStorage :: Storage -> Sink (Piece BL.ByteString) IO ()
189sinkStorage s = do
190 awaitForever $ \ piece ->
191 liftIO $ writePiece piece s
192
193-- | This function can be used to generate 'InfoDict' from a set of
194-- opened files.
195genPieceInfo :: Storage -> IO PieceInfo
196genPieceInfo s = do
197 hashes <- sourceStorage s $= C.map hashPiece $$ C.sinkLbs
198 return $ PieceInfo (pieceLen s) (HashList (BL.toStrict hashes))
199
200-- | Verify specific piece using infodict hash list.
201verifyPiece :: Storage -> PieceInfo -> PieceIx -> IO Bool
202verifyPiece s pinfo pix = do
203 piece <- unsafeReadPiece pix s
204 return $! checkPieceLazy pinfo piece
205
206-- | Verify storage.
207--
208-- Throws 'InvalidSize' if piece info size do not match with storage
209-- piece size.
210--
211getBitfield :: Storage -> PieceInfo -> IO Bitfield
212getBitfield s @ Storage {..} pinfo @ PieceInfo {..}
213 | pieceLen /= piPieceLength = throwIO (InvalidSize piPieceLength)
214 | otherwise = M.foldM checkPiece (BF.haveNone total) [0..total - 1]
215 where
216 total = totalPieces s
217
218 checkPiece :: Bitfield -> PieceIx -> IO Bitfield
219 checkPiece bf pix = do
220 valid <- verifyPiece s pinfo pix
221 return $ if valid then BF.insert pix bf else bf
diff --git a/bittorrent/src/System/Torrent/Tree.hs b/bittorrent/src/System/Torrent/Tree.hs
new file mode 100644
index 00000000..41cfb360
--- /dev/null
+++ b/bittorrent/src/System/Torrent/Tree.hs
@@ -0,0 +1,83 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Directory tree can be used to easily manipulate file layout info.
9--
10{-# LANGUAGE FlexibleInstances #-}
11{-# LANGUAGE TemplateHaskell #-}
12{-# LANGUAGE DeriveDataTypeable #-}
13module System.Torrent.Tree
14 ( -- * Directory tree
15 DirTree (..)
16
17 -- * Construction
18 , build
19
20 -- * Query
21 , System.Torrent.Tree.lookup
22 , lookupDir
23 , fileNumber
24 , dirNumber
25 ) where
26
27import Data.ByteString as BS
28import Data.ByteString.Char8 as BC
29import Data.Foldable
30import Data.List as L
31import Data.Map as M
32import Data.Monoid
33
34import Data.Torrent
35
36
37-- | 'DirTree' is more convenient form of 'LayoutInfo'.
38data DirTree a = Dir { children :: Map ByteString (DirTree a) }
39 | File { node :: FileInfo a }
40 deriving Show
41
42-- | Build directory tree from a list of files.
43build :: LayoutInfo -> DirTree ()
44build SingleFile {liFile = FileInfo {..}} = Dir
45 { children = M.singleton fiName (File fi) }
46 where
47 fi = FileInfo fiLength fiMD5Sum ()
48build MultiFile {..} = Dir $ M.singleton liDirName files
49 where
50 files = Dir $ M.fromList $ L.map mkFileEntry liFiles
51 mkFileEntry FileInfo {..} = (L.head fiName, ent) -- TODO FIXME
52 where
53 ent = File $ FileInfo fiLength fiMD5Sum ()
54
55--decompress :: DirTree () -> [FileInfo ()]
56--decompress = undefined
57
58-- TODO pretty print
59
60-- | Lookup file by path.
61lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a)
62lookup [] t = Just t
63lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m
64 = System.Torrent.Tree.lookup ps subTree
65lookup _ _ = Nothing
66
67-- | Lookup directory by path.
68lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)]
69lookupDir ps d = do
70 subTree <- System.Torrent.Tree.lookup ps d
71 case subTree of
72 File _ -> Nothing
73 Dir es -> Just $ M.toList es
74
75-- | Get total count of files in directory and subdirectories.
76fileNumber :: DirTree a -> Sum Int
77fileNumber File {..} = Sum 1
78fileNumber Dir {..} = foldMap fileNumber children
79
80-- | Get total count of directories in the directory and subdirectories.
81dirNumber :: DirTree a -> Sum Int
82dirNumber File {..} = Sum 0
83dirNumber Dir {..} = Sum 1 <> foldMap dirNumber children
diff --git a/bittorrent/tests/Config.hs b/bittorrent/tests/Config.hs
new file mode 100644
index 00000000..55e30867
--- /dev/null
+++ b/bittorrent/tests/Config.hs
@@ -0,0 +1,183 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE RecordWildCards #-}
3module Config
4 ( -- * Types
5 ClientName
6 , ClientOpts (..)
7 , EnvOpts (..)
8
9 -- * For test suite driver
10 , getOpts
11
12 -- * For item specs
13 , getEnvOpts
14 , getThisOpts
15 , getMyAddr
16
17 , getRemoteOpts
18 , withRemote
19 , withRemoteAddr
20
21 , getTestTorrent
22 ) where
23
24import Control.Monad
25import Network
26import Data.Default
27import Data.IORef
28import Data.List as L
29import Data.Maybe
30import Options.Applicative
31import System.Exit
32import System.Environment
33import System.IO.Unsafe
34import Test.Hspec
35
36import Data.Torrent
37import Network.BitTorrent.Address (IP, PeerAddr (PeerAddr), genPeerId)
38
39
40type ClientName = String
41
42
43#if !MIN_VERSION_network(2,6,3)
44instance Read PortNumber where
45 readsPrec = error "readsPrec"
46#endif
47
48data ClientOpts = ClientOpts
49 { peerPort :: PortNumber -- tcp port
50 , nodePort :: PortNumber -- udp port
51 }
52
53instance Default ClientOpts where
54 def = ClientOpts
55 { peerPort = 6881
56 , nodePort = 6881
57 }
58
59defRemoteOpts :: ClientOpts
60defRemoteOpts = def
61
62defThisOpts :: ClientOpts
63defThisOpts = def
64 { peerPort = 6882
65 , nodePort = 6882
66 }
67
68clientOptsParser :: Parser ClientOpts
69clientOptsParser = ClientOpts
70 <$> option auto
71 ( long "peer-port" <> short 'p'
72 <> value 6881 <> showDefault
73 <> metavar "NUM"
74 <> help "port to bind the specified bittorrent client"
75 )
76 <*> option auto
77 ( long "node-port" <> short 'n'
78 <> value 6881 <> showDefault
79 <> metavar "NUM"
80 <> help "port to bind node of the specified client"
81 )
82
83data EnvOpts = EnvOpts
84 { testClient :: Maybe ClientName
85 , testTorrents :: [FilePath]
86 , remoteOpts :: ClientOpts
87 , thisOpts :: ClientOpts
88 }
89
90instance Default EnvOpts where
91 def = EnvOpts
92 { testClient = Just "rtorrent"
93 , testTorrents = ["testfile.torrent"]
94 , remoteOpts = defRemoteOpts
95 , thisOpts = defThisOpts
96 }
97
98findConflicts :: EnvOpts -> [String]
99findConflicts EnvOpts {..}
100 | isNothing testClient = []
101 | peerPort remoteOpts == peerPort thisOpts = ["Peer port the same"]
102 | nodePort remoteOpts == nodePort thisOpts = ["Node port the same"]
103 | otherwise = []
104
105
106envOptsParser :: Parser EnvOpts
107envOptsParser = EnvOpts
108 <$> optional (strOption
109 ( long "bittorrent-client"
110 <> metavar "CLIENT"
111 <> help "torrent client to run"
112 ))
113 <*> pure []
114 <*> clientOptsParser
115 <*> clientOptsParser
116
117envOptsInfo :: ParserInfo EnvOpts
118envOptsInfo = info (helper <*> envOptsParser)
119 ( fullDesc
120 <> progDesc "The bittorrent library testsuite"
121 <> header ""
122 )
123
124-- do not modify this while test suite is running because spec items
125-- can run in parallel
126envOptsRef :: IORef EnvOpts
127envOptsRef = unsafePerformIO (newIORef def)
128
129-- | Should be used from spec items.
130getEnvOpts :: IO EnvOpts
131getEnvOpts = readIORef envOptsRef
132
133getThisOpts :: IO ClientOpts
134getThisOpts = thisOpts <$> getEnvOpts
135
136-- | Return 'Nothing' if remote client is not running.
137getRemoteOpts :: IO (Maybe ClientOpts)
138getRemoteOpts = do
139 EnvOpts {..} <- getEnvOpts
140 return $ const remoteOpts <$> testClient
141
142withRemote :: (ClientOpts -> Expectation) -> Expectation
143withRemote action = do
144 mopts <- getRemoteOpts
145 case mopts of
146 Nothing -> pendingWith "Remote client isn't running"
147 Just opts -> action opts
148
149withRemoteAddr :: (PeerAddr IP -> Expectation) -> Expectation
150withRemoteAddr action = do
151 withRemote $ \ ClientOpts {..} ->
152 action (PeerAddr Nothing "0.0.0.0" peerPort)
153
154getMyAddr :: IO (PeerAddr (Maybe IP))
155getMyAddr = do
156 ClientOpts {..} <- getThisOpts
157 pid <- genPeerId
158 return $ PeerAddr (Just pid) Nothing peerPort
159
160getTestTorrent :: IO Torrent
161getTestTorrent = do
162 EnvOpts {..} <- getEnvOpts
163 if L.null testTorrents
164 then error "getTestTorrent"
165 else fromFile ("res/" ++ L.head testTorrents)
166
167-- TODO fix EnvOpts parsing
168
169-- | Should be used by test suite driver.
170getOpts :: IO (EnvOpts, [String])
171getOpts = do
172 args <- getArgs
173-- case runParser SkipOpts envOptsParser args) (prefs idm) of
174 case (Right (def, args), ()) of
175 (Left err , _ctx) -> exitFailure
176 (Right (envOpts, hspecOpts), _ctx) -> do
177 let conflicts = findConflicts envOpts
178 unless (L.null conflicts) $ do
179 forM_ conflicts putStrLn
180 exitFailure
181
182 writeIORef envOptsRef envOpts
183 return (envOpts, hspecOpts)
diff --git a/bittorrent/tests/Data/TorrentSpec.hs b/bittorrent/tests/Data/TorrentSpec.hs
new file mode 100644
index 00000000..b4a280e4
--- /dev/null
+++ b/bittorrent/tests/Data/TorrentSpec.hs
@@ -0,0 +1,139 @@
1{-# LANGUAGE TypeSynonymInstances #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3{-# LANGUAGE StandaloneDeriving #-}
4{-# OPTIONS -fno-warn-orphans #-}
5module Data.TorrentSpec (spec) where
6import Control.Applicative
7import Data.BEncode
8import Data.ByteString as BS
9import Data.ByteString.Lazy as BL
10import Data.Convertible
11import Data.Maybe
12import Data.Monoid
13import Data.Time
14import Network.URI
15import System.FilePath
16import System.Posix.Types
17import Test.Hspec
18import Test.QuickCheck
19import Test.QuickCheck.Instances ()
20
21import Data.Torrent
22import Network.BitTorrent.CoreSpec ()
23
24
25pico :: Gen (Maybe NominalDiffTime)
26pico = oneof
27 [ pure Nothing
28 , (Just . fromIntegral) <$> (arbitrary :: Gen Int)
29 ]
30
31instance Arbitrary COff where
32 arbitrary = fromIntegral <$> (arbitrary :: Gen Int)
33
34instance Arbitrary URIAuth where
35 arbitrary = URIAuth <$> arbitrary <*> arbitrary <*> arbitrary
36
37instance Arbitrary URI where
38 arbitrary
39 = pure $ fromJust $ parseURI "http://ietf.org/1737.txt?a=1&b=h#123"
40
41instance Arbitrary InfoHash where
42 arbitrary = do
43 bs <- BS.pack <$> vectorOf 20 arbitrary
44 pure $ either (const (error "arbitrary infohash")) id $ safeConvert bs
45
46instance Arbitrary a => Arbitrary (FileInfo a) where
47 arbitrary = FileInfo <$> arbitrary <*> arbitrary <*> arbitrary
48
49instance Arbitrary LayoutInfo where
50 arbitrary = oneof
51 [ SingleFile <$> arbitrary
52 , MultiFile <$> arbitrary <*> arbitrary
53 ]
54
55instance Arbitrary a => Arbitrary (Piece a) where
56 arbitrary = Piece <$> arbitrary <*> arbitrary
57
58instance Arbitrary HashList where
59 arbitrary = HashList <$> arbitrary
60
61instance Arbitrary PieceInfo where
62 arbitrary = PieceInfo <$> arbitrary <*> arbitrary
63
64instance Arbitrary InfoDict where
65 arbitrary = infoDictionary <$> arbitrary <*> arbitrary <*> arbitrary
66
67instance Arbitrary Torrent where
68 arbitrary = Torrent <$> arbitrary
69 <*> arbitrary <*> arbitrary <*> arbitrary
70 <*> pico <*> arbitrary <*> arbitrary
71 <*> arbitrary
72 <*> arbitrary <*> pure Nothing <*> arbitrary
73
74instance Arbitrary Magnet where
75 arbitrary = Magnet <$> arbitrary <*> arbitrary
76 <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
77 <*> arbitrary <*> arbitrary <*> pure mempty
78
79type TestPair = (FilePath, String)
80
81-- TODO add a few more torrents here
82torrentList :: [TestPair]
83torrentList =
84 [ ( "res" </> "dapper-dvd-amd64.iso.torrent"
85 , "0221caf96aa3cb94f0f58d458e78b0fc344ad8bf")
86 ]
87
88infohashSpec :: (FilePath, String) -> Spec
89infohashSpec (filepath, expectedHash) = do
90 it ("should match " ++ filepath) $ do
91 torrent <- fromFile filepath
92 let actualHash = show $ idInfoHash $ tInfoDict torrent
93 actualHash `shouldBe` expectedHash
94
95magnetEncoding :: Magnet -> IO ()
96magnetEncoding m = parseMagnet (renderMagnet m) `shouldBe` Just m
97
98data T a = T
99
100prop_properBEncode :: Show a => BEncode a => Eq a
101 => T a -> a -> IO ()
102prop_properBEncode _ expected = actual `shouldBe` Right expected
103 where
104 actual = decode $ BL.toStrict $ encode expected
105
106spec :: Spec
107spec = do
108 describe "info hash" $ do
109 mapM_ infohashSpec torrentList
110
111 describe "accumPosition" $ do
112 it "" $ property $ \ p1 p2 p3 s1 s2 s3 ->
113 accumPositions [(p1, s1), (p2, s2), (p3, s3)]
114 `shouldBe` [(p1, (0, s1)), (p2, (s1, s2)), (p3, (s1 + s2, s3))]
115
116 describe "FileInfo" $ do
117 it "properly bencoded" $ property $
118 prop_properBEncode (T :: T (FileInfo BS.ByteString))
119
120 describe "LayoutInfo" $ do
121 it "properly bencoded" $ property $
122 prop_properBEncode (T :: T LayoutInfo)
123
124 describe "Torrent" $ do
125 it "property bencoded" $ property $
126 prop_properBEncode (T :: T Torrent)
127
128 describe "Magnet" $ do
129 it "properly encoded" $ property $ magnetEncoding
130
131 it "parse base32" $ do
132 let magnet = "magnet:?xt=urn:btih:CT76LXJDDCH5LS2TUHKH6EUJ3NYKX4Y6"
133 let ih = "CT76LXJDDCH5LS2TUHKH6EUJ3NYKX4Y6"
134 parseMagnet magnet `shouldBe` Just (nullMagnet ih)
135
136 it "parse base16" $ do
137 let magnet = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567"
138 let ih = "0123456789abcdef0123456789abcdef01234567"
139 parseMagnet magnet `shouldBe` Just (nullMagnet ih)
diff --git a/bittorrent/tests/Main.hs b/bittorrent/tests/Main.hs
new file mode 100644
index 00000000..5ed953da
--- /dev/null
+++ b/bittorrent/tests/Main.hs
@@ -0,0 +1,97 @@
1{-# LANGUAGE RecordWildCards #-}
2module Main where
3import Control.Exception
4import Control.Monad
5import Data.Functor
6import Data.Maybe
7import System.Directory
8import System.Exit
9import System.Environment
10import System.FilePath
11import System.Process
12import Text.Printf
13import Test.Hspec
14
15import Config
16import qualified Spec as Generated
17
18
19type Command = String
20type Descr = (ClientName, ClientOpts -> FilePath -> Command)
21
22torrents :: [FilePath]
23torrents =
24 [ "dapper-dvd-amd64-iso.torrent"
25 , "pkg.torrent"
26 , "testfile.torrent"
27 ]
28
29rtorrentSessionDir :: String
30rtorrentSessionDir = "rtorrent-sessiondir"
31
32sessionName :: String -- screen session name
33sessionName = "bittorrent-testsuite"
34
35tmpDir :: FilePath
36tmpDir = "res"
37
38clients :: [Descr]
39clients =
40 [ ("rtorrent"
41 , \ ClientOpts {..} tfile -> printf
42 "rtorrent -p %i-%i -O dht=on -O dht_port=%i -O session=%s %s"
43 (fromEnum peerPort) (fromEnum peerPort) (fromEnum nodePort)
44 rtorrentSessionDir tfile
45 )
46 ]
47
48setupEnv :: EnvOpts -> IO (Maybe ())
49setupEnv EnvOpts {..}
50 | Just client <- testClient
51 , Just mkCmd <- lookup client clients = do
52 _ <- printf "Setting up %s\n" client
53
54 let torrentPath = "testfile.torrent"
55 let runner = printf "screen -dm -S %s %s" sessionName
56 (mkCmd remoteOpts torrentPath)
57
58 wd <- getCurrentDirectory
59 createDirectoryIfMissing True (wd </> tmpDir </> rtorrentSessionDir)
60 _ <- createProcess (shell runner) { cwd = Just (wd </> tmpDir) }
61
62 return (Just ())
63
64 | Just client <- testClient = do
65 _ <- printf "Bad client `%s`, use one of %s\n" client (show (fst <$> clients))
66 return Nothing
67
68 | otherwise = do
69 _ <- printf "Running without remote client\n"
70 return (Just ())
71
72terminateEnv :: IO ()
73terminateEnv = do
74 wd <- getCurrentDirectory
75 removeDirectoryRecursive (wd </> tmpDir </> rtorrentSessionDir)
76 _ <- printf "closing screen session: %s\n" sessionName
77 _ <- system (printf "screen -S %s -X quit" sessionName)
78 return ()
79
80runTestSuite :: [String] -> IO ExitCode
81runTestSuite args = do
82 _ <- printf "running hspec test suite with args: %s\n" (show args)
83 catch (withArgs args (hspec Generated.spec) >> return ExitSuccess) return
84
85withEnv :: EnvOpts -> IO a -> IO a
86withEnv opts action = bracket (setupEnv opts) terminate (const action)
87 where
88 terminate running = do
89 when (isJust running) $ do
90 terminateEnv
91
92main :: IO ()
93main = do
94 (envOpts, suiteArgs) <- getOpts
95 withEnv envOpts $ do
96 code <- runTestSuite suiteArgs
97 exitWith code
diff --git a/bittorrent/tests/Network/BitTorrent/Client/HandleSpec.hs b/bittorrent/tests/Network/BitTorrent/Client/HandleSpec.hs
new file mode 100644
index 00000000..d51bab02
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Client/HandleSpec.hs
@@ -0,0 +1,19 @@
1module Network.BitTorrent.Client.HandleSpec (spec) where
2import Data.Default
3import Test.Hspec
4
5import Data.Torrent
6import Network.BitTorrent.Client
7import Network.BitTorrent.Client.Handle
8
9data_dir :: FilePath
10data_dir = "data"
11
12spec :: Spec
13spec = do
14 describe "openMagnet" $ do
15 it "should add new infohash to index" $ do
16 simpleClient $ do
17 _ <- openMagnet data_dir (nullMagnet def)
18 _ <- getHandle def
19 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/CoreSpec.hs b/bittorrent/tests/Network/BitTorrent/CoreSpec.hs
new file mode 100644
index 00000000..5bf900b2
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/CoreSpec.hs
@@ -0,0 +1,305 @@
1{-# LANGUAGE FlexibleInstances #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.BitTorrent.CoreSpec (spec) where
4import Control.Applicative
5import Data.BEncode as BE
6import Data.ByteString.Lazy as BL
7import Data.IP
8import Data.Serialize as S
9import Data.String
10import Data.Text.Encoding as T
11import Data.Word
12import Network
13import Test.Hspec
14import Test.QuickCheck
15import Test.QuickCheck.Instances ()
16
17import Network.BitTorrent.Address
18
19
20instance Arbitrary IPv4 where
21 arbitrary = do
22 a <- choose (0, 255)
23 b <- choose (0, 255)
24 c <- choose (0, 255)
25 d <- choose (0, 255)
26 return $ toIPv4 [a, b, c, d]
27
28instance Arbitrary IPv6 where
29 arbitrary = do
30 a <- choose (0, fromIntegral (maxBound :: Word16))
31 b <- choose (0, fromIntegral (maxBound :: Word16))
32 c <- choose (0, fromIntegral (maxBound :: Word16))
33 d <- choose (0, fromIntegral (maxBound :: Word16))
34 e <- choose (0, fromIntegral (maxBound :: Word16))
35 f <- choose (0, fromIntegral (maxBound :: Word16))
36 g <- choose (0, fromIntegral (maxBound :: Word16))
37 h <- choose (0, fromIntegral (maxBound :: Word16))
38 return $ toIPv6 [a, b, c, d, e, f, g, h]
39
40instance Arbitrary IP where
41 arbitrary = frequency
42 [ (1, IPv4 <$> arbitrary)
43 , (1, IPv6 <$> arbitrary)
44 ]
45
46instance Arbitrary PortNumber where
47 arbitrary = fromIntegral <$> (arbitrary :: Gen Word16)
48
49instance Arbitrary PeerId where
50 arbitrary = oneof
51 [ azureusStyle defaultClientId defaultVersionNumber
52 <$> (T.encodeUtf8 <$> arbitrary)
53 , shadowStyle 'X' defaultVersionNumber
54 <$> (T.encodeUtf8 <$> arbitrary)
55 ]
56
57instance Arbitrary a => Arbitrary (PeerAddr a) where
58 arbitrary = PeerAddr <$> arbitrary <*> arbitrary <*> arbitrary
59
60instance Arbitrary NodeId where
61 arbitrary = fromString <$> vector 20
62
63instance Arbitrary a => Arbitrary (NodeAddr a) where
64 arbitrary = NodeAddr <$> arbitrary <*> arbitrary
65
66instance Arbitrary a => Arbitrary (NodeInfo a) where
67 arbitrary = NodeInfo <$> arbitrary <*> arbitrary
68
69spec :: Spec
70spec = do
71 describe "PeerId" $ do
72 it "properly bencoded" $ do
73 BE.decode "20:01234567890123456789"
74 `shouldBe` Right ("01234567890123456789" :: PeerId)
75
76 describe "PortNumber" $ do
77 it "properly serialized" $ do
78 S.decode "\x1\x2" `shouldBe` Right (258 :: PortNumber)
79 S.encode (258 :: PortNumber) `shouldBe` "\x1\x2"
80
81 it "properly bencoded" $ do
82 BE.decode "i80e" `shouldBe` Right (80 :: PortNumber)
83
84 it "fail if port number is invalid" $ do
85 (BE.decode "i-10e" :: BE.Result PortNumber)
86 `shouldBe`
87 Left "fromBEncode: unable to decode PortNumber: -10"
88
89 (BE.decode "i70000e" :: BE.Result PortNumber)
90 `shouldBe`
91 Left "fromBEncode: unable to decode PortNumber: 70000"
92
93 describe "Peer IPv4" $ do
94 it "properly serialized" $ do
95 S.decode "\x1\x2\x3\x4" `shouldBe` Right (toIPv4 [1, 2, 3, 4])
96 S.encode (toIPv4 [1, 2, 3, 4]) `shouldBe` "\x1\x2\x3\x4"
97
98 it "properly serialized (iso)" $ property $ \ ip -> do
99 S.decode (S.encode ip) `shouldBe` Right (ip :: IPv4)
100
101 it "properly bencoded" $ do
102 BE.decode "11:168.192.0.1" `shouldBe` Right (toIPv4 [168, 192, 0, 1])
103 BE.encode (toIPv4 [168, 192, 0, 1]) `shouldBe` "11:168.192.0.1"
104
105 it "properly bencoded (iso)" $ property $ \ ip ->
106 BE.decode (BL.toStrict (BE.encode ip)) `shouldBe` Right (ip :: IPv4)
107
108 it "fail gracefully on invalid strings" $ do
109 BE.decode "3:1.1" `shouldBe`
110 (Left "fromBEncode: unable to decode IP: 1.1" :: BE.Result IPv4)
111
112 it "fail gracefully on invalid bencode" $ do
113 BE.decode "i10e" `shouldBe`
114 (Left "fromBEncode: unable to decode IP: addr should be a bstring"
115 :: BE.Result IPv4)
116
117 describe "Peer IPv6" $ do
118 it "properly serialized" $ do
119 S.decode "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10"
120 `shouldBe`
121 Right ("102:304:506:708:90a:b0c:d0e:f10" :: IPv6)
122
123 S.encode ("102:304:506:708:90a:b0c:d0e:f10" :: IPv6)
124 `shouldBe`
125 "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10"
126
127 it "properly serialized (iso)" $ property $ \ ip ->
128 S.decode (S.encode ip) `shouldBe` Right (ip :: IPv6)
129
130 it "properly bencoded" $ do
131 BE.decode "3:::1" `shouldBe` Right (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])
132 BE.encode (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]) `shouldBe`
133 "23:00:00:00:00:00:00:00:01"
134
135 BE.decode "23:00:00:00:00:00:00:00:01"
136 `shouldBe`
137 Right (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])
138
139 it "properly bencoded iso" $ property $ \ ip ->
140 BE.decode (BL.toStrict (BE.encode ip)) `shouldBe` Right (ip :: IPv4)
141
142 it "fail gracefully on invalid strings" $ do
143 BE.decode "4:g::1" `shouldBe`
144 (Left "fromBEncode: unable to decode IP: g::1" :: BE.Result IPv6)
145
146 it "fail gracefully on invalid bencode" $ do
147 BE.decode "i10e" `shouldBe`
148 (Left "fromBEncode: unable to decode IP: addr should be a bstring"
149 :: BE.Result IPv6)
150
151
152 describe "Peer IP" $ do
153 it "properly serialized IPv6" $ do
154 S.decode "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10"
155 `shouldBe`
156 Right ("102:304:506:708:90a:b0c:d0e:f10" :: IP)
157
158 S.encode ("102:304:506:708:90a:b0c:d0e:f10" :: IP)
159 `shouldBe`
160 "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10"
161
162 it "properly serialized (iso) IPv6" $ property $ \ ip ->
163 S.decode (S.encode ip) `shouldBe` Right (ip :: IP)
164
165 it "properly serialized IPv4" $ do
166 S.decode "\x1\x2\x3\x4" `shouldBe` Right (IPv4 $ toIPv4 [1, 2, 3, 4])
167 S.encode (toIPv4 [1, 2, 3, 4]) `shouldBe` "\x1\x2\x3\x4"
168
169 it "properly serialized (iso) IPv4" $ property $ \ ip -> do
170 S.decode (S.encode ip) `shouldBe` Right (ip :: IP)
171
172 it "properly bencoded" $ do
173 BE.decode "11:168.192.0.1" `shouldBe`
174 Right (IPv4 (toIPv4 [168, 192, 0, 1]))
175
176 BE.decode "3:::1" `shouldBe` Right
177 (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]))
178
179 BE.encode (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])) `shouldBe`
180 "23:00:00:00:00:00:00:00:01"
181
182 BE.decode "23:00:00:00:00:00:00:00:01"
183 `shouldBe`
184 Right (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]))
185
186 it "properly bencoded iso" $ property $ \ ip ->
187 BE.decode (BL.toStrict (BE.encode ip)) `shouldBe` Right (ip :: IP)
188
189 it "fail gracefully on invalid strings" $ do
190 BE.decode "4:g::1" `shouldBe`
191 (Left "fromBEncode: unable to decode IP: g::1" :: BE.Result IP)
192
193 it "fail gracefully on invalid bencode" $ do
194 BE.decode "i10e" `shouldBe`
195 (Left "fromBEncode: unable to decode IP: addr should be a bstring"
196 :: BE.Result IP)
197
198 describe "PeerAddr" $ do
199 it "IsString" $ do
200 ("127.0.0.1:80" :: PeerAddr IP)
201 `shouldBe` PeerAddr Nothing "127.0.0.1" 80
202
203 ("127.0.0.1:80" :: PeerAddr IPv4)
204 `shouldBe` PeerAddr Nothing "127.0.0.1" 80
205
206 ("[::1]:80" :: PeerAddr IP)
207 `shouldBe` PeerAddr Nothing "::1" 80
208
209 ("[::1]:80" :: PeerAddr IPv6)
210 `shouldBe` PeerAddr Nothing "::1" 80
211
212 it "properly bencoded (iso)" $ property $ \ addr ->
213 BE.decode (BL.toStrict (BE.encode addr))
214 `shouldBe` Right (addr :: PeerAddr IP)
215
216
217 it "properly bencoded (ipv4)" $ do
218 BE.decode "d2:ip11:168.192.0.1\
219 \7:peer id20:01234567890123456789\
220 \4:porti6881e\
221 \e"
222 `shouldBe`
223 Right (PeerAddr (Just "01234567890123456789")
224 (IPv4 (toIPv4 [168, 192, 0, 1]))
225 6881)
226
227 it "properly bencoded (ipv6)" $ do
228 BE.decode "d2:ip3:::1\
229 \7:peer id20:01234567890123456789\
230 \4:porti6881e\
231 \e"
232 `shouldBe`
233 Right (PeerAddr (Just "01234567890123456789")
234 (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]))
235 6881)
236
237 it "peer id is optional" $ do
238 BE.decode "d2:ip11:168.192.0.1\
239 \4:porti6881e\
240 \e"
241 `shouldBe`
242 Right (PeerAddr Nothing (IPv4 (toIPv4 [168, 192, 0, 1])) 6881)
243
244 it "has sock addr for both ipv4 and ipv6" $ do
245 show (peerSockAddr "128.0.0.1:80") `shouldBe` "128.0.0.1:80"
246 show (peerSockAddr "[::1]:8080" ) `shouldBe` "[::1]:8080"
247
248 describe "NodeId" $ do
249 it "properly serialized" $ do
250 S.decode "mnopqrstuvwxyz123456"
251 `shouldBe` Right ("mnopqrstuvwxyz123456" :: NodeId)
252
253 S.encode ("mnopqrstuvwxyz123456" :: NodeId)
254 `shouldBe` "mnopqrstuvwxyz123456"
255
256 it "properly serialized (iso)" $ property $ \ nid ->
257 S.decode (S.encode nid) `shouldBe`
258 Right (nid :: NodeId)
259
260 describe "NodeAddr" $ do
261 it "properly serialized" $ do
262 S.decode "\127\0\0\1\1\2" `shouldBe`
263 Right ("127.0.0.1:258" :: NodeAddr IPv4)
264
265 it "properly serialized (iso)" $ property $ \ nid ->
266 S.decode (S.encode nid) `shouldBe`
267 Right (nid :: NodeAddr IPv4)
268
269 describe "NodeInfo" $ do
270 it "properly serialized" $ do
271 S.decode "mnopqrstuvwxyz123456\
272 \\127\0\0\1\1\2" `shouldBe` Right
273 (NodeInfo "mnopqrstuvwxyz123456" "127.0.0.1:258" :: NodeInfo IPv4)
274
275 it "properly serialized (iso)" $ property $ \ nid ->
276 S.decode (S.encode nid) `shouldBe`
277 Right (nid :: NodeInfo IPv4)
278
279 -- see <http://bittorrent.org/beps/bep_0020.html>
280 describe "Fingerprint" $ do
281 it "decode mainline encoded peer id" $ do
282 fingerprint "M4-3-6--xxxxxxxxxxxx" `shouldBe` "Mainline-4.3.6"
283 fingerprint "M4-20-8-xxxxxxxxxxxx" `shouldBe` "Mainline-4.20.8"
284
285 it "decode azureus encoded peer id" $ do
286 fingerprint "-AZ2060-xxxxxxxxxxxx" `shouldBe` "Azureus-2060"
287 fingerprint "-BS0000-xxxxxxxxxxxx" `shouldBe` "BTSlave-0"
288
289 it "decode Shad0w style peer id" $ do
290 fingerprint "S58B-----xxxxxxxxxxx" `shouldBe` "Shadow-5.8.11"
291 fingerprint "T58B-----xxxxxxxxxxx" `shouldBe` "BitTornado-5.8.11"
292
293 it "decode bitcomet style peer id" $ do
294 fingerprint "exbc01xxxxxxxxxxxxxx" `shouldBe` "BitComet-48.49"
295 fingerprint "FUTB01xxxxxxxxxxxxxx" `shouldBe` "BitComet-48.49"
296 fingerprint "exbc01LORDxxxxxxxxxx" `shouldBe` "BitLord-48.49"
297
298 it "decode opera style peer id" $ do
299 fingerprint "OP0123xxxxxxxxxxxxxx" `shouldBe` "Opera-123"
300
301 it "decode ML donkey style peer id" $ do
302 fingerprint "-ML2.7.2-xxxxxxxxxxx" `shouldBe` "MLdonkey-0"
303
304-- TODO XBT, Bits on Wheels, Queen Bee, BitTyrant, TorrenTopia,
305-- BitSpirit, Rufus, G3 Torrent, FlashGet \ No newline at end of file
diff --git a/bittorrent/tests/Network/BitTorrent/DHT/MessageSpec.hs b/bittorrent/tests/Network/BitTorrent/DHT/MessageSpec.hs
new file mode 100644
index 00000000..6f3c7489
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHT/MessageSpec.hs
@@ -0,0 +1,221 @@
1{-# LANGUAGE RecordWildCards #-}
2module Network.BitTorrent.DHT.MessageSpec (spec) where
3import Control.Monad.Reader
4import Control.Monad.Logger
5import Control.Concurrent
6import Data.BEncode as BE
7import Data.ByteString.Lazy as BL
8import Data.Default
9import Data.List as L
10import Data.Maybe
11import Network.BitTorrent.Address
12import Network.BitTorrent.DHT.Message
13import qualified Network.KRPC as KRPC (def)
14import Network.KRPC hiding (def)
15import Network.Socket (PortNumber)
16import Test.Hspec
17import Test.QuickCheck
18import System.Timeout
19
20import Data.TorrentSpec ()
21import Network.BitTorrent.CoreSpec ()
22import Network.BitTorrent.DHT.TokenSpec ()
23
24-- Arbitrary queries and responses.
25instance Arbitrary Ping where arbitrary = pure Ping
26instance Arbitrary FindNode where arbitrary = FindNode <$> arbitrary
27instance Arbitrary ip => Arbitrary (NodeFound ip) where arbitrary = NodeFound <$> arbitrary
28instance Arbitrary GetPeers where arbitrary = GetPeers <$> arbitrary
29instance Arbitrary ip => Arbitrary (GotPeers ip) where arbitrary = GotPeers <$> arbitrary <*> arbitrary
30instance Arbitrary Announce where arbitrary = Announce <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
31instance Arbitrary Announced where arbitrary = pure Announced
32instance Arbitrary x => Arbitrary (Query x) where arbitrary = Query <$> arbitrary <*> arbitrary <*> arbitrary
33instance Arbitrary x => Arbitrary (Response x) where arbitrary = Response <$> arbitrary <*> arbitrary
34
35instance MonadLogger IO where
36 monadLoggerLog _ _ _ _ = return ()
37
38remoteAddr :: SockAddr
39remoteAddr = SockAddrInet 6881 (256 * 256 * 256 + 127)
40
41thisAddr :: SockAddr
42thisAddr = SockAddrInet 60000 (256 * 256 * 256 + 127)
43
44thisPort :: PortNumber
45thisPort = 60001
46
47rpc :: ReaderT (Manager IO) IO a -> IO a
48rpc action = do
49 withManager KRPC.def thisAddr [] $ runReaderT $ do
50 listen
51 action
52
53isQueryError :: QueryFailure -> Bool
54isQueryError _ = True
55
56prop_bencode :: Eq a => Show a => BEncode a => a -> Expectation
57prop_bencode x = BE.decode (BL.toStrict (BE.encode x)) `shouldBe` Right x
58
59retry :: Int -> IO (Maybe a) -> IO (Maybe a)
60retry 0 _ = return Nothing
61retry n a = do
62 res <- a
63 case res of
64 Just _ -> return res
65 Nothing -> threadDelay (100 * 1000) >> retry (n-1) a
66
67spec :: Spec
68spec = do
69 context ("you need running DHT node at " ++ show remoteAddr) $ do
70 it "is running" $ do
71 running <- retry 5 $ timeout (100 * 1000) $ do
72 nid <- genNodeId
73 Response _remoteAddr Ping <-
74 rpc (query remoteAddr (Query nid False Ping))
75 return ()
76 running `shouldSatisfy` isJust
77
78 describe "ping" $ do
79 it "properly bencoded" $ do
80 BE.decode "d2:id20:abcdefghij0123456789e"
81 `shouldBe` Right (Query "abcdefghij0123456789" False Ping)
82
83 BE.encode (Query "abcdefghij0123456789" False Ping)
84 `shouldBe` "d2:id20:abcdefghij0123456789e"
85
86 BE.decode "d2:id20:mnopqrstuvwxyz123456e"
87 `shouldBe` Right (Response "mnopqrstuvwxyz123456" Ping)
88
89 BE.encode (Response "mnopqrstuvwxyz123456" Ping)
90 `shouldBe` "d2:id20:mnopqrstuvwxyz123456e"
91
92 it "properly bencoded (iso)" $ property $ \ nid -> do
93 prop_bencode (Query nid False Ping)
94 prop_bencode (Response nid Ping)
95
96 it "does compatible with existing DHT" $ do
97 nid <- genNodeId
98 Response _remoteAddr Ping <- rpc (query remoteAddr (Query nid False Ping))
99 return ()
100
101 describe "find_node" $ do
102 it "properly bencoded" $ do
103 BE.decode "d2:id20:abcdefghij0123456789\
104 \6:target20:mnopqrstuvwxyz123456e"
105 `shouldBe` Right (Query "abcdefghij0123456789" False
106 (FindNode "mnopqrstuvwxyz123456"))
107
108 BE.encode (Query "abcdefghij0123456789" False
109 (FindNode "mnopqrstuvwxyz123456"))
110 `shouldBe`
111 "d2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e"
112
113 let naddr = "127.0.0.1:258" :: NodeAddr IPv4
114 let nid = "0123456789abcdefghij"
115 let nid' = "mnopqrstuvwxyz123456"
116 BE.decode "d2:id20:0123456789abcdefghij\
117 \5:nodes26:mnopqrstuvwxyz123456\127\0\0\1\1\2\
118 \e"
119 `shouldBe` Right (Response nid (NodeFound [NodeInfo nid' naddr]))
120
121 it "properly bencoded (iso)" $ property $ \ nid x xs -> do
122 prop_bencode (Query nid False (FindNode x))
123 prop_bencode (Response nid (NodeFound (xs :: [NodeInfo IPv4] )))
124
125 it "does compatible with existing DHT" $ do
126 nid <- genNodeId
127 Response _remoteAddr (NodeFound xs) <- rpc $ do
128 query remoteAddr (Query nid False (FindNode nid))
129 L.length (xs :: [NodeInfo IPv4]) `shouldSatisfy` (> 0)
130
131 describe "get_peers" $ do
132 it "properly bencoded" $ do
133 BE.decode "d2:id20:abcdefghij0123456789\
134 \9:info_hash20:mnopqrstuvwxyz123456\
135 \e"
136 `shouldBe` Right (Query "abcdefghij0123456789" False
137 (GetPeers "mnopqrstuvwxyz123456"))
138
139 BE.decode "d2:id20:abcdefghij0123456789\
140 \5:token8:aoeusnth\
141 \6:valuesl6:\127\0\0\1\1\2\&6:\192\168\1\100\1\2e\
142 \e"
143 `shouldBe` Right (Response "abcdefghij0123456789"
144 (GotPeers (Right [ "127.0.0.1:258" :: PeerAddr IPv4
145 , "192.168.1.100:258"
146 ]) "aoeusnth"))
147
148 BE.decode "d2:id20:abcdefghij0123456789\
149 \5:nodes26:mnopqrstuvwxyz123456\127\0\0\1\1\2\
150 \5:token8:aoeusnth\
151 \e"
152 `shouldBe` Right (Response "abcdefghij0123456789"
153 (GotPeers
154 { peers = Left [NodeInfo "mnopqrstuvwxyz123456" "127.0.0.1:258"
155 :: NodeInfo IPv4]
156 , grantedToken = "aoeusnth"
157 }))
158
159 it "properly bencoded (iso)" $ property $ \ nid topic exs token -> do
160 prop_bencode (Query nid False (GetPeers topic))
161 let _ = exs :: Either [NodeInfo IPv4] [PeerAddr IPv4]
162 let nullPeerId paddr = paddr {peerId = Nothing}
163 let nullPeerIds = either Left (Right . L.map nullPeerId)
164 prop_bencode (Response nid (GotPeers (nullPeerIds exs) token))
165
166 it "does compatible with existing DHT" $ do
167 nid <- genNodeId
168 Response _remoteId (GotPeers {..})
169 <- rpc $ query remoteAddr (Query nid False (GetPeers def))
170 let _ = peers :: Either [NodeInfo IPv4] [PeerAddr IPv4]
171 either L.length L.length peers `shouldSatisfy` (> 0)
172
173 describe "announce" $ do
174 it "properly bencoded" $ do
175 BE.decode "d2:id20:abcdefghij0123456789\
176 \9:info_hash20:mnopqrstuvwxyz123456\
177 \4:porti6881e\
178 \5:token8:aoeusnth\
179 \e" `shouldBe` Right
180 (Query "abcdefghij0123456789" False
181 (Announce False "mnopqrstuvwxyz123456" Nothing 6881 "aoeusnth"))
182
183 BE.decode "d2:id20:abcdefghij0123456789\
184 \12:implied_porti1e\
185 \9:info_hash20:mnopqrstuvwxyz123456\
186 \4:porti6881e\
187 \5:token8:aoeusnth\
188 \e" `shouldBe` Right
189 (Query "abcdefghij0123456789" False
190 (Announce True "mnopqrstuvwxyz123456" Nothing 6881 "aoeusnth"))
191
192
193 BE.decode "d2:id20:mnopqrstuvwxyz123456e"
194 `shouldBe` Right
195 (Response "mnopqrstuvwxyz123456" Announced)
196
197 it "properly bencoded (iso)" $ property $ \ nid flag topic port token -> do
198 prop_bencode (Query nid False (Announce flag topic Nothing port token))
199 prop_bencode (Response nid (Announced))
200
201
202 it "does compatible with existing DHT" $ do
203 nid <- genNodeId
204 Response _remoteId Announced <- rpc $ do
205 Response _ GotPeers {..} <- query remoteAddr (Query nid False (GetPeers def))
206 let _ = peers :: Either [NodeInfo IPv4] [PeerAddr IPv4]
207 query remoteAddr (Query nid False (Announce False def Nothing thisPort grantedToken))
208 return ()
209
210 it "does fail on invalid token" $ do
211 nid <- genNodeId
212 (rpc $ do
213 Response _ GotPeers {..} <- query remoteAddr (Query nid False (GetPeers def))
214 let _ = peers :: Either [NodeInfo IPv4] [PeerAddr IPv4]
215 let invalidToken = ""
216 let q :: MonadKRPC h m => SockAddr -> Query Announce
217 -> m (Response Announced)
218 q = query
219 q remoteAddr (Query nid False (Announce False def Nothing thisPort invalidToken)))
220 `shouldThrow` isQueryError
221 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/DHT/QuerySpec.hs b/bittorrent/tests/Network/BitTorrent/DHT/QuerySpec.hs
new file mode 100644
index 00000000..93f78263
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHT/QuerySpec.hs
@@ -0,0 +1,105 @@
1{-# LANGUAGE RecordWildCards #-}
2module Network.BitTorrent.DHT.QuerySpec (spec) where
3import Control.Applicative
4import Control.Exception
5import Control.Monad.Reader
6import Data.Conduit as C
7import Data.Conduit.List as CL
8import Data.Default
9import Data.List as L
10import Test.Hspec
11
12import Network.BitTorrent.Address
13import Network.BitTorrent.DHT
14import Network.BitTorrent.DHT.Session
15import Network.BitTorrent.DHT.Query
16
17import Network.BitTorrent.DHT.TestData
18
19
20myAddr :: NodeAddr IPv4
21myAddr = "0.0.0.0:0"
22
23nullLogger :: LogFun
24nullLogger _ _ _ _ = return ()
25
26--simpleLogger :: LogFun
27--simpleLogger _ t _ _ = print t
28
29simpleDHT :: [NodeHandler IPv4] -> DHT IPv4 a -> IO a
30simpleDHT hs m =
31 bracket (newNode hs def myAddr nullLogger Nothing) closeNode $ \ node ->
32 runDHT node m
33
34getBootInfo :: IO (NodeInfo IPv4)
35getBootInfo = do
36 startAddr <- resolveHostName (L.head defaultBootstrapNodes)
37 simpleDHT [] $ fmap fst (pingQ startAddr)
38
39spec :: Spec
40spec = parallel $ do
41 describe "environment" $ do
42 describe "test node" $ do
43 it "is alive" $ do
44 _ <- getBootInfo
45 return ()
46
47 describe "handlers" $ do
48 it "" $ pendingWith "need to setup 2 DHT locally"
49
50 describe "basic queries" $ do
51 it "ping" $ do
52 _ <- getBootInfo
53 return ()
54
55 it "findNode" $ do
56 startInfo <- getBootInfo
57 _ <- simpleDHT [] $ do
58 nid <- myNodeIdAccordingTo (read "8.8.8.8:6881")
59 findNodeQ nid startInfo
60 return ()
61
62 it "getPeers" $ do
63 startInfo <- getBootInfo
64 peers <- simpleDHT [] $ do
65 nid <- myNodeIdAccordingTo (read "8.8.8.8:6881")
66
67 -- we should not run getPeers query on boot node, because
68 -- it may not support it
69 Right infos <- findNodeQ nid startInfo
70
71 when (L.null infos) $
72 error "boot node malfunction"
73
74 -- at least one node should reply
75 queryParallel $ do
76 getPeersQ (entryHash (L.head testTorrents)) <$> infos
77
78 peers `shouldSatisfy` (not . L.null)
79
80 it "announce" $ do
81 bootNode <- getBootInfo
82 _ <- simpleDHT [] $ do
83 let ih = entryHash (L.head testTorrents)
84 Right nodes <- findNodeQ ih bootNode
85
86 when (L.null nodes) $
87 error "boot node malfunction"
88
89 queryParallel $ do
90 announceQ ih (nodePort myAddr) <$> nodes
91
92 return ()
93
94 describe "iterative queries" $ do
95 forM_ testTorrents $ \ TestEntry {..} -> do
96 context entryName $ do
97
98 it "get at least 10 unique peers for each infohash" $ do
99 bootNode <- getBootInfo
100 peers <- simpleDHT [] $ do
101 Right startNodes <- findNodeQ entryHash bootNode
102 sourceList [startNodes] $=
103 search entryHash (getPeersQ entryHash) $=
104 CL.concat $$ CL.take 10
105 L.length peers `shouldBe` 10
diff --git a/bittorrent/tests/Network/BitTorrent/DHT/RoutingSpec.hs b/bittorrent/tests/Network/BitTorrent/DHT/RoutingSpec.hs
new file mode 100644
index 00000000..07a906ba
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHT/RoutingSpec.hs
@@ -0,0 +1,77 @@
1{-# LANGUAGE ScopedTypeVariables #-}
2{-# LANGUAGE FlexibleContexts #-}
3module Network.BitTorrent.DHT.RoutingSpec (spec) where
4import Control.Applicative
5import Control.Monad.State
6import Data.Default
7import Data.List as L
8import Data.Maybe
9import Test.Hspec
10import Test.QuickCheck
11
12import Network.BitTorrent.Address
13import Network.BitTorrent.DHT.Routing as T
14
15import Network.BitTorrent.CoreSpec hiding (spec)
16
17
18type Network ip = [NodeAddr ip]
19
20data Env ip = Env
21 { currentTime :: Timestamp
22 , network :: Network ip
23 } deriving Show
24
25type Simulation ip = State (Env ip)
26
27runSimulation :: Eq ip => Env ip -> Routing ip a -> Maybe a
28runSimulation e m = evalState (runRouting ping closest timestamp m) e
29 where
30 ping addr = gets (L.elem addr . network)
31 closest nid = error "runSimulation"
32 timestamp = gets currentTime
33
34instance Arbitrary ip => Arbitrary (Env ip) where
35 arbitrary = Env <$> arbitrary <*> (vector nodeCount)
36 where
37 nodeCount = 1000
38
39instance (Arbitrary ip, Eq ip) => Arbitrary (Table ip) where
40 arbitrary = do
41 thisId <- arbitrary
42 bucketN <- choose (1, 20)
43 let table = nullTable thisId bucketN
44
45-- nodeN <- (`mod` bucketN) <$> arbitrary
46-- nodes <- vector nodeN
47
48 node <- arbitrary
49 mt <- do
50 env <- arbitrary
51 return $ runSimulation env $ do
52 (_,t') <- T.insert (currentTime env) (TryInsert node) table
53 return t' :: Routing ip (Table ip)
54 --(foldM (flip fillTable) table nodes)
55 return (fromJust mt)
56-- where
57-- fillTable x t = do
58-- t' <- T.insert x t
59-- return $ if T.full t' then t else t'
60
61spec :: Spec
62spec = do
63 describe "size" $ do
64 it "null table is empty" $ do
65 T.size (nullTable def 2 :: Table IPv4) `shouldBe` 0
66
67 it "the same node never appear in different buckets" $ property $ \ t -> do
68 let xss = T.toList (t :: Table Int)
69 let justOnce x = L.length (L.filter (L.elem x) xss) == 1
70 L.all justOnce (L.concat xss)
71
72 it "insert is idemponent" $ property $ \ (e :: Env Int) n t -> do
73 let ins :: NodeInfo Int -> Table Int -> Routing Int (Table Int)
74 ins n t = snd <$> T.insert (currentTime e) (TryInsert n) t
75 let t1 = runSimulation e (ins n t)
76 let t2 = runSimulation e (ins n t >>= ins n)
77 t1 `shouldBe` t2
diff --git a/bittorrent/tests/Network/BitTorrent/DHT/SessionSpec.hs b/bittorrent/tests/Network/BitTorrent/DHT/SessionSpec.hs
new file mode 100644
index 00000000..32e4c158
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHT/SessionSpec.hs
@@ -0,0 +1,110 @@
1{-# LANGUAGE ScopedTypeVariables #-}
2module Network.BitTorrent.DHT.SessionSpec (spec) where
3import Control.Applicative
4import Control.Concurrent
5import Control.Exception
6import Control.Monad.Reader
7import Control.Monad.Trans.Resource
8import Data.Conduit.Lazy
9import Data.Default
10import Data.List as L
11import Test.Hspec
12import Test.QuickCheck
13
14import Network.BitTorrent.Address
15import Network.BitTorrent.DHT
16import Network.BitTorrent.DHT.Message
17import Network.BitTorrent.DHT.Session
18import Network.BitTorrent.DHT.Query
19
20import Data.TorrentSpec ()
21import Network.BitTorrent.CoreSpec ()
22import Network.BitTorrent.DHT.TokenSpec ()
23
24
25myAddr :: NodeAddr IPv4
26myAddr = "127.0.0.1:60000"
27
28simpleDHT :: DHT IPv4 a -> IO a
29simpleDHT m =
30 bracket (newNode defaultHandlers def myAddr nullLogger Nothing) closeNode $ \ node ->
31 runDHT node m
32
33isRight :: Either a b -> Bool
34isRight (Left _) = False
35isRight (Right _) = True
36
37isLeft :: Either a b -> Bool
38isLeft = not . isRight
39
40nullLogger :: LogFun
41nullLogger _ _ _ _ = return ()
42
43spec :: Spec
44spec = do
45 describe "session" $ do
46 it "is active until closeNode called" $ do
47 node <- newNode [] def myAddr nullLogger Nothing
48 runDHT node monadActive `shouldReturn` True
49 runDHT node monadActive `shouldReturn` True
50 closeNode node
51 runDHT node monadActive `shouldReturn` False
52
53 describe "tokens" $ do
54 it "should not complain about valid token" $
55 property $ \ (addrs :: [NodeAddr IPv4]) -> do
56 isOks <- simpleDHT $ do
57 forM addrs $ \ addr -> do
58 token <- grantToken addr
59 checkToken addr token
60 L.and isOks `shouldBe` True
61
62 it "should complain about invalid token" $
63 property $ \ (addr :: NodeAddr IPv4) token -> do
64 isOk <- simpleDHT (checkToken addr token)
65 isOk `shouldBe` False
66
67 describe "routing table" $
68 it "accept any node entry when table is empty" $
69 property $ \ (nid :: NodeId) -> do
70 let info = NodeInfo nid myAddr
71 closest <- simpleDHT $ do
72 _ <- insertNode info Nothing
73 liftIO $ yield
74 getClosest nid
75 closest `shouldSatisfy` L.elem info
76
77 describe "peer storage" $ do
78 it "should return nodes, if there are no peers" $ property $ \ ih -> do
79 res <- simpleDHT $ do getPeerList ih
80 res `shouldSatisfy` isLeft
81
82 it "should return peers, if any" $ property $ \ ih addr -> do
83 res <- simpleDHT $ do
84 insertPeer ih addr
85 getPeerList ih
86 res `shouldSatisfy` isRight
87
88 describe "topic storage" $ do
89 it "should not grow indefinitely" $ do
90 pending
91
92 describe "messaging" $ do
93 describe "queryNode" $ do
94 it "should always ping this node" $ do
95 (rid, tid) <- simpleDHT $ do
96 (remoteId, Ping) <- queryNode myAddr Ping
97 thisId <- myNodeIdAccordingTo (read "8.8.8.8:6881")
98 return (remoteId, thisId)
99 rid `shouldBe` tid
100
101 describe "queryParallel" $ do
102 it "should handle parallel requests" $ do
103 (nid, resps) <- simpleDHT $ do
104 me <- myNodeIdAccordingTo (read "8.8.8.8:6881")
105 ( (,) me ) <$> queryParallel (L.replicate 100 $ queryNode myAddr Ping)
106 resps `shouldSatisfy` L.all (== (nid, Ping))
107
108 describe "(<@>) operator" $ do
109 it "" $
110 pending
diff --git a/bittorrent/tests/Network/BitTorrent/DHT/TestData.hs b/bittorrent/tests/Network/BitTorrent/DHT/TestData.hs
new file mode 100644
index 00000000..e9473cbb
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHT/TestData.hs
@@ -0,0 +1,45 @@
1module Network.BitTorrent.DHT.TestData
2 ( TestEntry (..)
3 , testTorrents
4 ) where
5
6import Data.Torrent
7
8data TestEntry = TestEntry
9 { entryName :: String
10 , entryHash :: InfoHash
11 , entryPeers :: Int -- ^ approximate number of peers, may change with time
12 }
13
14testTorrents :: [TestEntry]
15testTorrents =
16 [ TestEntry
17 { entryName = "Automate with Arduino, Android..."
18 , entryHash = "8c0433e541dc5d1cfc095799cef171cd4eb586f7"
19 , entryPeers = 300
20 }
21
22 , TestEntry
23 { entryName = "Beginning Programming with Java For Dummies"
24 , entryHash = "fd8967721731cc16c8b203a03e49ce839cecf184"
25 , entryPeers = 200
26 }
27
28 , TestEntry
29 { entryName = "The C Programming Language"
30 , entryHash = "146d13f090e50e97091dbbe5b37678dd1471cfad"
31 , entryPeers = 100
32 }
33
34 , TestEntry
35 { entryName = "The C++ Programming Language"
36 , entryHash = "8e8e8e6319031a22cff26d895afe050085c84a7f"
37 , entryPeers = 50
38 }
39
40 , TestEntry
41 { entryName = "Game and Graphics Programming for iOS..."
42 , entryHash = "703d0595b727fccbfaa3d03be25f57347ccfd6de"
43 , entryPeers = 30
44 }
45 ]
diff --git a/bittorrent/tests/Network/BitTorrent/DHT/TokenSpec.hs b/bittorrent/tests/Network/BitTorrent/DHT/TokenSpec.hs
new file mode 100644
index 00000000..a45d2212
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHT/TokenSpec.hs
@@ -0,0 +1,42 @@
1{-# LANGUAGE ScopedTypeVariables #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.BitTorrent.DHT.TokenSpec (spec) where
4import Control.Applicative
5import Data.List as L
6import Data.String
7import Test.Hspec
8import Test.QuickCheck
9
10import Network.BitTorrent.Address
11import Network.BitTorrent.CoreSpec ()
12import Network.BitTorrent.DHT.Token as T
13
14
15instance Arbitrary Token where
16 arbitrary = fromString <$> arbitrary
17
18instance Arbitrary TokenMap where
19 arbitrary = tokens <$> arbitrary
20
21repeatN :: Int -> (a -> a) -> (a -> a)
22repeatN n f = L.foldr (.) id $ L.replicate n f
23
24spec :: Spec
25spec = do
26 describe "Token" $ do
27 return ()
28
29 describe "TokenMap" $ do
30 it "is keeping any granted token in current session" $
31 property $ \ (addr :: NodeAddr IPv4) m ->
32 T.member addr (T.lookup addr m) m
33
34 it "is keeping any granted token in next session" $
35 property $ \ (addr :: NodeAddr IPv4) m ->
36 T.member addr (T.lookup addr m) (T.update m)
37
38 -- can fail with some small probability
39 it "is rejecting any outdated tokens" $
40 property $ \ (addr :: NodeAddr IPv4) m k -> not $
41 let n = min 100 (abs k + 2) in
42 T.member addr (T.lookup addr m) (repeatN n T.update m) \ No newline at end of file
diff --git a/bittorrent/tests/Network/BitTorrent/DHTSpec.hs b/bittorrent/tests/Network/BitTorrent/DHTSpec.hs
new file mode 100644
index 00000000..77160eb5
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/DHTSpec.hs
@@ -0,0 +1,60 @@
1module Network.BitTorrent.DHTSpec (spec) where
2import Control.Exception
3import Control.Monad
4import Data.Default
5import Data.List as L
6import Test.Hspec
7import System.Timeout
8
9import Data.Torrent
10import Network.BitTorrent.DHT
11
12
13partialBootstrapTimeout :: Int
14partialBootstrapTimeout = 10 * 1000000
15
16opts :: Options
17opts = def { optBucketCount = 1 }
18
19-- NOTE to shorten test cases run time include only "good" infohashes
20-- with many nodes
21existingInfoHashes :: [InfoHash]
22existingInfoHashes =
23 [
24 ]
25
26-- TODO use Test.Hspec.parallel
27
28spec :: Spec
29spec = do
30 describe "bootstrapping" $ do
31 it "should resolve all default bootstrap nodes" $ do
32 nodes <- forM defaultBootstrapNodes resolveHostName
33 _ <- evaluate nodes
34 return ()
35
36 it "partial bootstrapping should finish in less than 10 seconds" $ do
37 node <- resolveHostName (L.head defaultBootstrapNodes)
38 res <- timeout partialBootstrapTimeout $ do
39 dht opts def fullLogging $ do
40 bootstrap Nothing [node]
41 isBootstrapped
42 res `shouldBe` Just True
43
44 describe "initialization" $ do
45 it "should be bootstrapped after restore process" $ do
46 pending
47
48 describe "lookup" $ do
49 describe "for any existing infohash" $ do
50 forM_ existingInfoHashes $ \ ih -> do
51 context (show ih) $ do
52 it "should find peers" $ do
53 pending
54
55 describe "insert" $ do
56 it "should return this peer if announced" $ do
57 pending
58
59 describe "delete" $ do
60 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Exchange/BitfieldSpec.hs b/bittorrent/tests/Network/BitTorrent/Exchange/BitfieldSpec.hs
new file mode 100644
index 00000000..1ba772f6
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Exchange/BitfieldSpec.hs
@@ -0,0 +1,14 @@
1{-# OPTIONS -fno-warn-orphans #-}
2module Network.BitTorrent.Exchange.BitfieldSpec (spec) where
3import Control.Applicative
4import Data.ByteString.Arbitrary
5import Test.Hspec
6import Test.QuickCheck
7
8import Network.BitTorrent.Exchange.Bitfield
9
10instance Arbitrary Bitfield where
11 arbitrary = fromBitmap . fromABS <$> arbitrary
12
13spec :: Spec
14spec = return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Exchange/BlockSpec.hs b/bittorrent/tests/Network/BitTorrent/Exchange/BlockSpec.hs
new file mode 100644
index 00000000..2dc8e0b8
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Exchange/BlockSpec.hs
@@ -0,0 +1,35 @@
1module Network.BitTorrent.Exchange.BlockSpec (spec) where
2import Control.Applicative
3import Control.Exception
4import Data.Maybe
5import Test.Hspec
6import Test.QuickCheck
7import Test.QuickCheck.Instances ()
8
9import Network.BitTorrent.Exchange.Block as Block
10
11
12instance Arbitrary a => Arbitrary (Block a) where
13 arbitrary = Block <$> arbitrary <*> arbitrary <*> arbitrary
14
15instance Arbitrary BlockIx where
16 arbitrary = BlockIx <$> arbitrary <*> arbitrary <*> arbitrary
17
18instance Arbitrary Bucket where
19 arbitrary = do
20 s <- arbitrary `suchThat` (> 0)
21 chunks <- arbitrary
22 return $ Block.fromList s chunks
23
24isSomeException :: SomeException -> Bool
25isSomeException = const True
26
27spec :: Spec
28spec = do
29 describe "empty" $ do
30 it "should fail on bad size" $ do
31 evaluate (Block.empty (-1)) `shouldThrow` isSomeException
32
33 describe "toPiece" $ do
34 it "render to piece when it is full" $ property $ \ bkt ->
35 full bkt == isJust (toPiece bkt) \ No newline at end of file
diff --git a/bittorrent/tests/Network/BitTorrent/Exchange/ConnectionSpec.hs b/bittorrent/tests/Network/BitTorrent/Exchange/ConnectionSpec.hs
new file mode 100644
index 00000000..d654cda1
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Exchange/ConnectionSpec.hs
@@ -0,0 +1,58 @@
1{-# LANGUAGE RecordWildCards #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.BitTorrent.Exchange.ConnectionSpec (spec) where
4import Control.Applicative
5import Control.Monad.Trans
6import Data.Default
7import Test.Hspec
8import Test.QuickCheck
9
10import Data.Torrent
11import Network.BitTorrent.Address
12import Network.BitTorrent.Exchange.Connection
13import Network.BitTorrent.Exchange.Message
14
15import Config
16import Network.BitTorrent.Exchange.MessageSpec ()
17
18nullSession :: InfoHash -> PeerId -> SessionLink ()
19nullSession ih pid = SessionLink ih pid Nothing Nothing ()
20
21instance Arbitrary Options where
22 arbitrary = return def
23
24instance Arbitrary ConnectionPrefs where
25 arbitrary = ConnectionPrefs <$> arbitrary <*> pure def
26 <*> arbitrary <*> arbitrary
27
28withWire :: ConnectionPrefs -> Wire () () -> IO ()
29withWire prefs wire =
30 withRemote $ \ ClientOpts {..} -> do
31 pid <- genPeerId
32 t <- getTestTorrent
33 let ih = idInfoHash (tInfoDict t)
34 let cfg = ConnectionConfig prefs (nullSession ih pid) (wire)
35 let addr = PeerAddr Nothing "127.0.0.1" peerPort
36 connectWire addr cfg
37
38spec :: Spec
39spec = do
40 describe "connectWire" $ do
41 it "can establish connection with all possible preferences" $
42 property $ \ prefs -> do
43 withWire prefs (return ())
44
45 it "must not connect with invalid topic" $ do
46 pending
47
48 describe "acceptWire" $ do
49 it "" $ do
50 pending
51
52 describe "messaging" $ do
53 it "first message is bitfield" $ do
54 withWire def $ do
55 msg <- recvMessage
56 let isBitfield (Available (Bitfield _)) = True
57 isBitfield _ = False
58 liftIO $ msg `shouldSatisfy` isBitfield
diff --git a/bittorrent/tests/Network/BitTorrent/Exchange/DownloadSpec.hs b/bittorrent/tests/Network/BitTorrent/Exchange/DownloadSpec.hs
new file mode 100644
index 00000000..d46f2034
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Exchange/DownloadSpec.hs
@@ -0,0 +1,59 @@
1{-# LANGUAGE RecordWildCards #-}
2module Network.BitTorrent.Exchange.DownloadSpec (spec) where
3import Control.Concurrent
4import Data.ByteString as BS
5import Data.ByteString.Lazy as BL
6import Test.Hspec
7import Test.QuickCheck
8
9import Data.BEncode as BE
10import Data.Torrent as Torrent
11import Network.BitTorrent.Address
12import Network.BitTorrent.Exchange.Download
13import Network.BitTorrent.Exchange.Message
14
15import Config
16import Network.BitTorrent.CoreSpec ()
17
18
19placeholderAddr :: PeerAddr IP
20placeholderAddr = "0.0.0.0:0"
21
22chunkBy :: Int -> BS.ByteString -> [BS.ByteString]
23chunkBy s bs
24 | BS.null bs = []
25 | otherwise = BS.take s bs : chunkBy s (BS.drop s bs)
26
27withUpdates :: Updates s a -> IO a
28withUpdates m = do
29 Torrent {..} <- getTestTorrent
30 let infoDictLen = fromIntegral $ BL.length $ BE.encode tInfoDict
31 --mvar <- newMVar (nullStatus infoDictLen)
32 --runUpdates mvar placeholderAddr m
33 undefined
34
35simulateFetch :: InfoDict -> Updates s (Maybe InfoDict)
36simulateFetch dict = undefined
37
38spec :: Spec
39spec = do
40 describe "scheduleBlock" $ do
41 it "never schedule the same index twice" $ do
42 pending
43
44 describe "resetPending" $ do
45 it "" $ do
46 pending
47
48 describe "cancelPending" $ do
49 it "must not throw an exception if cancel the same piece twice" $ do
50 pending
51
52 describe "pushBlock" $ do
53 it "assemble infodict from chunks" $ do
54 Torrent {..} <- getTestTorrent
55 mdict <- withUpdates $ simulateFetch tInfoDict
56 mdict `shouldBe` Just tInfoDict
57
58 it "must throw an exception if block if not requested" $ do
59 pending \ No newline at end of file
diff --git a/bittorrent/tests/Network/BitTorrent/Exchange/MessageSpec.hs b/bittorrent/tests/Network/BitTorrent/Exchange/MessageSpec.hs
new file mode 100644
index 00000000..d615b1ff
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Exchange/MessageSpec.hs
@@ -0,0 +1,102 @@
1{-# OPTIONS_GHC -fno-warn-orphans #-}
2module Network.BitTorrent.Exchange.MessageSpec (spec) where
3import Control.Applicative
4import Control.Exception
5import Data.ByteString as BS
6import Data.List as L
7import Data.Set as S
8import Data.Serialize as S
9import Data.String
10import Test.Hspec
11import Test.QuickCheck
12
13import Data.TorrentSpec ()
14import Network.BitTorrent.Exchange.BitfieldSpec ()
15import Network.BitTorrent.CoreSpec ()
16import Network.BitTorrent.Address ()
17import Network.BitTorrent.Exchange.BlockSpec ()
18import Network.BitTorrent.Exchange.Message
19
20instance Arbitrary Extension where
21 arbitrary = elements [minBound .. maxBound]
22
23instance Arbitrary Caps where
24 arbitrary = toCaps <$> arbitrary
25
26instance Arbitrary ExtendedExtension where
27 arbitrary = elements [minBound .. maxBound]
28
29instance Arbitrary ExtendedCaps where
30 arbitrary = toCaps <$> arbitrary
31
32instance Arbitrary ProtocolName where
33 arbitrary = fromString <$> (arbitrary `suchThat` ((200 <) . L.length))
34
35instance Arbitrary Handshake where
36 arbitrary = Handshake <$> arbitrary <*> arbitrary
37 <*> arbitrary <*> arbitrary
38
39instance Arbitrary StatusUpdate where
40 arbitrary = frequency
41 [ (1, Choking <$> arbitrary)
42 , (1, Interested <$> arbitrary)
43 ]
44
45instance Arbitrary Available where
46 arbitrary = frequency
47 [ (1, Have <$> arbitrary)
48 , (1, Bitfield <$> arbitrary)
49 ]
50
51instance Arbitrary Transfer where
52 arbitrary = frequency
53 [ (1, Request <$> arbitrary)
54 , (1, Piece <$> arbitrary)
55 , (1, Cancel <$> arbitrary)
56 ]
57
58instance Arbitrary FastMessage where
59 arbitrary = frequency
60 [ (1, pure HaveAll)
61 , (1, pure HaveNone)
62 , (1, SuggestPiece <$> arbitrary)
63 , (1, RejectRequest <$> arbitrary)
64 , (1, AllowedFast <$> arbitrary)
65 ]
66
67instance Arbitrary Message where
68 arbitrary = frequency
69 [ (1, pure KeepAlive)
70 , (1, Status <$> arbitrary)
71 , (1, Available <$> arbitrary)
72 , (1, Transfer <$> arbitrary)
73 , (1, Fast <$> arbitrary)
74 ]
75
76-- TODO test extension protocol
77
78spec :: Spec
79spec = do
80 describe "Caps" $ do
81 it "set-like container" $ property $ \ exts ->
82 L.all (`allowed` (toCaps exts :: Caps)) exts
83
84 it "preserve items" $ property $ \ extSet ->
85 S.fromList (fromCaps (toCaps (S.toList extSet) :: Caps))
86 `shouldBe` extSet
87
88 describe "ByteStats" $ do
89 it "preserve size" $ property $ \ msg ->
90 byteLength (stats msg) `shouldBe`
91 fromIntegral (BS.length (S.encode (msg :: Message)))
92
93 describe "ProtocolName" $ do
94 it "fail to construct invalid string" $ do
95 let str = L.replicate 500 'x'
96 evaluate (fromString str :: ProtocolName)
97 `shouldThrow`
98 errorCall ("fromString: ProtocolName too long: " ++ str)
99
100 describe "Handshake" $ do
101 it "properly serialized" $ property $ \ hs ->
102 S.decode (S.encode hs ) `shouldBe` Right (hs :: Handshake)
diff --git a/bittorrent/tests/Network/BitTorrent/Exchange/SessionSpec.hs b/bittorrent/tests/Network/BitTorrent/Exchange/SessionSpec.hs
new file mode 100644
index 00000000..bf5b95a1
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Exchange/SessionSpec.hs
@@ -0,0 +1,64 @@
1{-# LANGUAGE RecordWildCards #-}
2module Network.BitTorrent.Exchange.SessionSpec (spec) where
3import Test.Hspec
4
5import Data.Torrent
6import Network.BitTorrent.Address
7import Network.BitTorrent.Exchange.Session
8
9import Config
10
11
12nullLogger :: LogFun
13nullLogger _ _ x _ = print x
14
15simpleSession :: InfoDict -> (Session -> IO ()) -> IO ()
16simpleSession dict action = do
17 withRemoteAddr $ \ addr -> do
18 myAddr <- getMyAddr
19 ses <- newSession nullLogger myAddr "" (Right dict)
20 connect addr ses
21 action ses
22 closeSession ses
23
24spec :: Spec
25spec = do
26 describe "construction" $ do
27 describe "newSession" $ do
28 it "" $ do
29 pending
30
31 describe "closeSession" $ do
32 it "" $ do
33 pending
34
35 describe "connection set" $ do
36 describe "connect" $ do
37 it "" $ do
38 pending
39
40 describe "establish" $ do
41 it "" $ do
42 pending
43
44 describe "exchange" $ do
45 describe "metadata" $ do
46 it "should fetch info dictionary" $ do
47 Torrent {..} <- getTestTorrent
48 simpleSession tInfoDict $ \ ses -> do
49 dict <- waitMetadata ses
50 dict `shouldBe` tInfoDict
51
52 it "should serve info dictionary" $ do
53 pending
54
55 describe "content" $ do
56 it "should fetch torrent content" $ do
57 Torrent {..} <- getTestTorrent
58 simpleSession tInfoDict $ \ ses -> do
59 pending
60-- st <- waitData ses
61-- verifyStorage st (idPieceInfo tInfoDict)
62
63 it "should serve torrent content" $ do
64 pending
diff --git a/bittorrent/tests/Network/BitTorrent/Internal/CacheSpec.hs b/bittorrent/tests/Network/BitTorrent/Internal/CacheSpec.hs
new file mode 100644
index 00000000..337e7add
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Internal/CacheSpec.hs
@@ -0,0 +1,7 @@
1module Network.BitTorrent.Internal.CacheSpec (spec) where
2import Test.Hspec
3
4spec :: Spec
5spec = do
6 describe "Cached" $ do
7 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Internal/ProgressSpec.hs b/bittorrent/tests/Network/BitTorrent/Internal/ProgressSpec.hs
new file mode 100644
index 00000000..acbfd84c
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Internal/ProgressSpec.hs
@@ -0,0 +1,13 @@
1{-# OPTIONS -fno-warn-orphans #-}
2module Network.BitTorrent.Internal.ProgressSpec (spec) where
3import Control.Applicative
4import Test.Hspec
5import Test.QuickCheck
6import Network.BitTorrent.Internal.Progress
7
8
9instance Arbitrary Progress where
10 arbitrary = Progress <$> arbitrary <*> arbitrary <*> arbitrary
11
12spec :: Spec
13spec = return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/ListSpec.hs b/bittorrent/tests/Network/BitTorrent/Tracker/ListSpec.hs
new file mode 100644
index 00000000..bba9d0e2
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/ListSpec.hs
@@ -0,0 +1,40 @@
1module Network.BitTorrent.Tracker.ListSpec (spec) where
2import Control.Exception
3import Data.Default
4import Data.Foldable as F
5import Data.List as L
6import Data.Maybe
7import Network.URI
8import Test.Hspec
9
10import Data.Torrent
11import Network.BitTorrent.Tracker.List
12import Network.BitTorrent.Tracker.RPC
13
14
15uris :: [URI]
16uris = fmap (fromJust . parseURI . renderURI) [1..10 :: Int]
17 where
18 renderURI n = "http://" ++ show n ++ ".org"
19
20list :: TrackerList ()
21list = trackerList def { tAnnounceList = Just [uris] }
22
23spec :: Spec
24spec = do
25 describe "TrackerList" $ do
26 it "shuffleTiers (may fail with very small probability)" $ do
27 list' <- shuffleTiers list
28 list' `shouldSatisfy` (/= list)
29
30 it "traverseAll" $ do
31 xs <- traverseAll (\ (uri, _) -> if uri == L.last uris
32 then throwIO (GenericException "")
33 else return ()) list
34 return ()
35
36 it "traverseTiers" $ do
37 xs' <- traverseTiers (\ (uri, _) -> if uri == L.last uris then return ()
38 else throwIO (GenericException "")) list
39
40 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/MessageSpec.hs b/bittorrent/tests/Network/BitTorrent/Tracker/MessageSpec.hs
new file mode 100644
index 00000000..29854d58
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/MessageSpec.hs
@@ -0,0 +1,173 @@
1{-# LANGUAGE RecordWildCards #-}
2{-# LANGUAGE ViewPatterns #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# OPTIONS -fno-warn-orphans #-}
5module Network.BitTorrent.Tracker.MessageSpec
6 ( spec
7 , arbitrarySample
8 ) where
9
10import Control.Applicative
11import Control.Exception
12import Data.BEncode as BE
13import Data.ByteString.Lazy as BL
14import Data.List as L
15import Data.Maybe
16import Test.Hspec
17import Test.QuickCheck
18
19import Data.TorrentSpec ()
20import Network.BitTorrent.Internal.ProgressSpec ()
21import Network.BitTorrent.Address ()
22import Network.BitTorrent.Address ()
23
24import Network.BitTorrent.Tracker.Message as Message
25import Network.BitTorrent.Address
26
27
28--prop_bencode :: Eq a => BEncode a => a -> Bool
29--prop_bencode a = BE.decode (BL.toStrict (BE.encode a)) == return a
30
31--prop_urlencode :: Eq a => URLDecoded a => URLEncoded a => a -> Bool
32--prop_urlencode a = urlDecode (T.pack (urlEncode a)) == a
33
34instance Arbitrary AnnounceEvent where
35 arbitrary = elements [minBound..maxBound]
36
37instance Arbitrary AnnounceQuery where
38 arbitrary = AnnounceQuery
39 <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
40 <*> arbitrary <*> arbitrary <*> arbitrary
41
42instance Arbitrary (PeerList IP) where
43 arbitrary = frequency
44 [ (1, (PeerList . maybeToList) <$> arbitrary)
45 , (1, (CompactPeerList . maybeToList . fmap zeroPeerId) <$> arbitrary)
46 ]
47
48 shrink ( PeerList xs) = PeerList <$> shrink xs
49 shrink (CompactPeerList xs) = CompactPeerList <$> shrink xs
50
51instance Arbitrary AnnounceInfo where
52 arbitrary = AnnounceInfo
53 <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
54 <*> arbitrary <*> arbitrary
55
56arbitrarySample :: Arbitrary a => IO a
57arbitrarySample = L.head <$> sample' arbitrary
58
59zeroPeerId :: PeerAddr a -> PeerAddr a
60zeroPeerId addr = addr { peerId = Nothing }
61
62spec :: Spec
63spec = do
64 describe "AnnounceQuery" $ do
65 it "properly url encoded" $ property $ \ q ->
66 parseAnnounceQuery (renderAnnounceQuery q)
67 `shouldBe` Right q
68
69 describe "PeerList" $ do
70 context "Non compact" $ do
71 it "properly encoded (both ipv4 and ipv6)" $ do
72 BE.decode "ld2:ip7:1.2.3.44:porti80eed2:ip3:::14:porti8080eee"
73 `shouldBe` Right
74 (PeerList ["1.2.3.4:80", "[::1]:8080"] :: PeerList IPv4)
75
76 it "properly encoded (iso)" $ property $ \ xs ->
77 BE.decode (BL.toStrict (BE.encode (PeerList xs :: PeerList IPv4)))
78 `shouldBe` Right (PeerList xs :: PeerList IPv4)
79
80 context "Compact" $ do
81 it "properly encodes (ipv4)" $ do
82 BE.decode "12:\x1\x2\x3\x4\x1\x2\x9\x8\x7\x6\x1\x2"
83 `shouldBe` Right
84 (CompactPeerList ["1.2.3.4:258", "9.8.7.6:258"] :: PeerList IPv4)
85
86 it "properly encodes (ipv6)" $ do
87 BE.decode "18:\x1\x2\x3\x4\x5\x6\x7\x8\x1\x2\x3\x4\x5\x6\x7\x8\x1\x2"
88 `shouldBe` Right
89 (CompactPeerList ["[102:304:506:708:102:304:506:708]:258"]
90 :: PeerList IPv6)
91
92 it "properly encoded (ipv4, iso)" $
93 property $ \ (fmap zeroPeerId -> xs) ->
94 BE.decode (BL.toStrict (BE.encode (CompactPeerList xs)))
95 `shouldBe` Right (CompactPeerList xs :: PeerList IPv4)
96
97 it "properly encoded (ipv6, iso)" $
98 property $ \ (fmap zeroPeerId -> xs) ->
99 BE.decode (BL.toStrict (BE.encode (CompactPeerList xs)))
100 `shouldBe` Right (CompactPeerList xs :: PeerList IPv6)
101
102 describe "AnnounceInfo" $ do
103 it "parses minimal sample" $ do
104 "d8:intervali0e5:peerslee"
105 `shouldBe`
106 AnnounceInfo Nothing Nothing 0 Nothing (PeerList []) Nothing
107
108 it "parses optional fields" $ do
109 "d8:completei1e\
110 \10:incompletei2e\
111 \8:intervali3e\
112 \12:min intervali4e\
113 \5:peersle\
114 \15:warning message3:str\
115 \e"
116 `shouldBe`
117 AnnounceInfo (Just 1) (Just 2) 3 (Just 4) (PeerList []) (Just "str")
118
119 it "parses failed response" $ do
120 "d14:failure reason10:any reasone"
121 `shouldBe`
122 Message.Failure "any reason"
123
124 it "fail if no peer list present" $ do
125 evaluate ("d8:intervali0ee" :: AnnounceInfo)
126 `shouldThrow`
127 errorCall "fromString: unable to decode AnnounceInfo: \
128 \required field `peers' not found"
129
130 it "parses `peer' list" $ do -- TODO
131 "d8:intervali0e\
132 \5:peersl\
133 \d2:ip7:1.2.3.4\
134 \4:porti80e\
135 \e\
136 \d2:ip3:::1\
137 \4:porti80e\
138 \e\
139 \e\
140 \e" `shouldBe`
141 let xs = PeerList ["1.2.3.4:80", "[::1]:80"] in
142 AnnounceInfo Nothing Nothing 0 Nothing xs Nothing
143
144 it "parses `peers6' list" $ do
145 "d8:intervali0e\
146 \5:peers0:\
147 \6:peers60:\
148 \e" `shouldBe`
149 AnnounceInfo Nothing Nothing 0 Nothing (CompactPeerList []) Nothing
150
151 it "fails on invalid combinations of the peer lists" $ do
152 BE.decode "d8:intervali0e\
153 \5:peers0:\
154 \6:peers6le\
155 \e"
156 `shouldBe` (Left
157 "PeerList: the `peers6' field value should contain \
158 \*compact* peer list" :: BE.Result AnnounceInfo)
159
160 BE.decode "d8:intervali0e\
161 \5:peersle\
162 \6:peers60:\
163 \e"
164 `shouldBe` (Left
165 "PeerList: non-compact peer list provided, \
166 \but the `peers6' field present" :: BE.Result AnnounceInfo)
167
168 it "properly bencoded (iso)" $ property $ \ info ->
169 BE.decode (BL.toStrict (BE.encode info))
170 `shouldBe` Right (info :: AnnounceInfo)
171
172 describe "Scrape" $ do
173 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/RPC/HTTPSpec.hs b/bittorrent/tests/Network/BitTorrent/Tracker/RPC/HTTPSpec.hs
new file mode 100644
index 00000000..e928f917
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/RPC/HTTPSpec.hs
@@ -0,0 +1,95 @@
1{-# LANGUAGE RecordWildCards #-}
2module Network.BitTorrent.Tracker.RPC.HTTPSpec (spec) where
3import Control.Monad
4import Data.Default
5import Data.List as L
6import Test.Hspec
7
8import Network.BitTorrent.Internal.Progress
9import Network.BitTorrent.Tracker.Message as Message
10import Network.BitTorrent.Tracker.RPC.HTTP
11
12import Network.BitTorrent.Tracker.TestData
13import Network.BitTorrent.Tracker.MessageSpec hiding (spec)
14
15
16validateInfo :: AnnounceQuery -> AnnounceInfo -> Expectation
17validateInfo _ (Message.Failure reason) = do
18 error $ "validateInfo: " ++ show reason
19validateInfo AnnounceQuery {..} AnnounceInfo {..} = do
20 return ()
21-- case respComplete <|> respIncomplete of
22-- Nothing -> return ()
23-- Just n -> n `shouldBe` L.length (getPeerList respPeers)
24
25isUnrecognizedScheme :: RpcException -> Bool
26isUnrecognizedScheme (RequestFailed _) = True
27isUnrecognizedScheme _ = False
28
29isNotResponding :: RpcException -> Bool
30isNotResponding (RequestFailed _) = True
31isNotResponding _ = False
32
33spec :: Spec
34spec = parallel $ do
35 describe "Manager" $ do
36 describe "newManager" $ do
37 it "" $ pending
38
39 describe "closeManager" $ do
40 it "" $ pending
41
42 describe "withManager" $ do
43 it "" $ pending
44
45 describe "RPC" $ do
46 describe "announce" $ do
47 it "must fail on bad uri scheme" $ do
48 withManager def $ \ mgr -> do
49 q <- arbitrarySample
50 announce mgr "magnet://foo.bar" q
51 `shouldThrow` isUnrecognizedScheme
52
53 describe "scrape" $ do
54 it "must fail on bad uri scheme" $ do
55 withManager def $ \ mgr -> do
56 scrape mgr "magnet://foo.bar" []
57 `shouldThrow` isUnrecognizedScheme
58
59 forM_ (L.filter isHttpTracker trackers) $ \ TrackerEntry {..} ->
60 context trackerName $ do
61
62 describe "announce" $ do
63 if tryAnnounce
64 then do
65 it "have valid response" $ do
66 withManager def $ \ mgr -> do
67-- q <- arbitrarySample
68 let ih = maybe def L.head hashList
69 let q = AnnounceQuery ih "-HS0003-203534.37420" 6000
70 (Progress 0 0 0) Nothing Nothing (Just Started)
71 info <- announce mgr trackerURI q
72 validateInfo q info
73 else do
74 it "should fail with RequestFailed" $ do
75 withManager def $ \ mgr -> do
76 q <- arbitrarySample
77 announce mgr trackerURI q
78 `shouldThrow` isNotResponding
79
80 describe "scrape" $ do
81 if tryScraping
82 then do
83 it "have valid response" $ do
84 withManager def $ \ mgr -> do
85 xs <- scrape mgr trackerURI [def]
86 L.length xs `shouldSatisfy` (>= 1)
87 else do
88 it "should fail with ScrapelessTracker" $ do
89 pending
90
91 when (not tryAnnounce) $ do
92 it "should fail with RequestFailed" $ do
93 withManager def $ \ mgr -> do
94 scrape mgr trackerURI [def]
95 `shouldThrow` isNotResponding
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/RPC/UDPSpec.hs b/bittorrent/tests/Network/BitTorrent/Tracker/RPC/UDPSpec.hs
new file mode 100644
index 00000000..73acb3fa
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/RPC/UDPSpec.hs
@@ -0,0 +1,144 @@
1{-# LANGUAGE RecordWildCards #-}
2module Network.BitTorrent.Tracker.RPC.UDPSpec (spec, rpcOpts) where
3import Control.Concurrent
4import Control.Concurrent.Async
5import Control.Exception
6import Control.Monad
7import Data.Default
8import Data.List as L
9import Data.Maybe
10import Test.Hspec
11
12import Network.BitTorrent.Address
13import Network.BitTorrent.Tracker.Message as Message
14
15import Network.BitTorrent.Tracker.TestData
16import Network.BitTorrent.Tracker.MessageSpec hiding (spec)
17import Network.BitTorrent.Tracker.RPC.UDP
18
19
20validateInfo :: AnnounceQuery -> AnnounceInfo -> Expectation
21validateInfo _ Message.Failure {} = error "validateInfo: failure"
22validateInfo AnnounceQuery {..} AnnounceInfo {..} = do
23 respComplete `shouldSatisfy` isJust
24 respIncomplete `shouldSatisfy` isJust
25 respMinInterval `shouldSatisfy` isNothing
26 respWarning `shouldSatisfy` isNothing
27 peerList `shouldSatisfy` L.all (isNothing . peerId)
28 where
29 peerList = getPeerList respPeers
30
31-- | Number of concurrent calls.
32rpcCount :: Int
33rpcCount = 100
34
35rpcOpts :: Options
36rpcOpts = def
37 { optMinTimeout = 1
38 , optMaxTimeout = 10
39 }
40
41isTimeoutExpired :: RpcException -> Bool
42isTimeoutExpired (TimeoutExpired _) = True
43isTimeoutExpired _ = False
44
45isSomeException :: SomeException -> Bool
46isSomeException _ = True
47
48isIOException :: IOException -> Bool
49isIOException _ = True
50
51spec :: Spec
52spec = parallel $ do
53 describe "newManager" $ do
54 it "should throw exception on zero optMaxPacketSize" $ do
55 let opts = def { optMaxPacketSize = 0 }
56 newManager opts `shouldThrow` isSomeException
57
58 it "should throw exception on zero optMinTimout" $ do
59 let opts = def { optMinTimeout = 0 }
60 newManager opts `shouldThrow` isSomeException
61
62 it "should throw exception on zero optMaxTimeout" $ do
63 let opts = def { optMaxTimeout = 0 }
64 newManager opts `shouldThrow` isSomeException
65
66 it "should throw exception on maxTimeout < minTimeout" $ do
67 let opts = def { optMinTimeout = 2, optMaxTimeout = 1 }
68 newManager opts `shouldThrow` isSomeException
69
70 it "should throw exception on zero optMultiplier" $ do
71 let opts = def { optMultiplier = 0 }
72 newManager opts `shouldThrow` isSomeException
73
74 describe "closeManager" $ do
75 it "unblock rpc calls" $ do
76 mgr <- newManager rpcOpts
77 _ <- forkIO $ do
78 threadDelay 10000000
79 closeManager mgr
80 q <- arbitrarySample
81 announce mgr (trackerURI badTracker) q `shouldThrow` (== ManagerClosed)
82
83 it "announce throw exception after manager closed" $ do
84 mgr <- newManager rpcOpts
85 closeManager mgr
86 q <- arbitrarySample
87 announce mgr (trackerURI badTracker) q `shouldThrow` isIOException
88
89 it "scrape throw exception after manager closed" $ do
90 mgr <- newManager rpcOpts
91 closeManager mgr
92 scrape mgr (trackerURI badTracker) [def] `shouldThrow` isIOException
93
94 describe "withManager" $ do
95 it "closesManager at exit" $ do
96 mgr <- withManager rpcOpts return
97 scrape mgr (trackerURI badTracker) [def] `shouldThrow` isSomeException
98
99 describe "RPC" $ do
100 describe "announce" $ do
101 it "must fail on bad scheme" $ do
102 withManager rpcOpts $ \ mgr -> do
103 q <- arbitrarySample
104 announce mgr "magnet://a.com" q
105 `shouldThrow` (== UnrecognizedScheme "magnet:")
106
107 describe "scrape" $ do
108 it "must fail on bad scheme" $ do
109 withManager rpcOpts $ \ mgr -> do
110 scrape mgr "magnet://a.com" []
111 `shouldThrow` (== UnrecognizedScheme "magnet:")
112
113 forM_ (L.filter isUdpTracker trackers) $ \ TrackerEntry {..} ->
114 context trackerName $ do
115
116 describe "announce" $ do
117 if tryAnnounce then do
118 it "have valid response" $ do
119 withManager rpcOpts $ \ mgr -> do
120 q <- arbitrarySample
121 announce mgr trackerURI q >>= validateInfo q
122 else do
123 it "should throw TimeoutExpired" $ do
124 withManager rpcOpts $ \ mgr -> do
125 q <- arbitrarySample
126 announce mgr trackerURI q `shouldThrow` isTimeoutExpired
127
128 describe "scrape" $ do
129 if tryScraping then do
130 it "have valid response" $ do
131 withManager rpcOpts $ \ mgr -> do
132 xs <- scrape mgr trackerURI [def]
133 L.length xs `shouldSatisfy` (>= 1)
134 else do
135 it "should throw TimeoutExpired" $ do
136 withManager rpcOpts $ \ mgr -> do
137 scrape mgr trackerURI [def] `shouldThrow` isTimeoutExpired
138
139 describe "Manager" $ do
140 when tryScraping $ do
141 it "should handle arbitrary intermixed concurrent queries" $ do
142 withManager rpcOpts $ \ mgr -> do
143 _ <- mapConcurrently (\ _ -> scrape mgr trackerURI [def]) [1..rpcCount]
144 return ()
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/RPCSpec.hs b/bittorrent/tests/Network/BitTorrent/Tracker/RPCSpec.hs
new file mode 100644
index 00000000..dfc13a1e
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/RPCSpec.hs
@@ -0,0 +1,79 @@
1{-# LANGUAGE RecordWildCards #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.BitTorrent.Tracker.RPCSpec (spec) where
4import Control.Applicative
5import Control.Monad
6import Data.Default
7import Data.List as L
8import Test.Hspec
9import Test.QuickCheck
10
11import Network.BitTorrent.Tracker.RPC as RPC
12
13import Network.BitTorrent.Tracker.TestData
14import Network.BitTorrent.Tracker.MessageSpec hiding (spec)
15import qualified Network.BitTorrent.Tracker.RPC.UDPSpec as UDP (rpcOpts)
16
17
18instance Arbitrary SAnnounceQuery where
19 arbitrary = SAnnounceQuery <$> arbitrary <*> arbitrary
20 <*> arbitrary <*> arbitrary
21
22rpcOpts :: Options
23rpcOpts = def
24 { optUdpRPC = UDP.rpcOpts
25 }
26
27matchUnrecognizedScheme :: String -> RpcException -> Bool
28matchUnrecognizedScheme x (UnrecognizedScheme scheme) = x == scheme
29matchUnrecognizedScheme _ _ = False
30
31spec :: Spec
32spec = parallel $ do
33 describe "Manager" $ do
34 describe "newManager" $ do
35 it "" $ pending
36
37 describe "closeManager" $ do
38 it "" $ pending
39
40 describe "withManager" $ do
41 it "" $ pending
42
43 describe "RPC" $ do
44 describe "announce" $ do
45 it "must fail on bad uri scheme" $ do
46 withManager rpcOpts def $ \ mgr -> do
47 q <- arbitrarySample
48 announce mgr "magnet://foo.bar" q
49 `shouldThrow` matchUnrecognizedScheme "magnet:"
50
51 describe "scrape" $ do
52 it "must fail on bad uri scheme" $ do
53 withManager rpcOpts def $ \ mgr -> do
54 scrape mgr "magnet://foo.bar" []
55 `shouldThrow` matchUnrecognizedScheme "magnet:"
56
57 forM_ trackers $ \ TrackerEntry {..} ->
58 context trackerName $ do
59
60 describe "announce" $ do
61 if tryAnnounce then do
62 it "have valid response" $ do
63 withManager rpcOpts def $ \ mgr -> do
64 q <- arbitrarySample
65 _ <- announce mgr trackerURI q
66 return ()
67 else do
68 it "should throw exception" $ do
69 pending
70
71 describe "scrape" $ do
72 if tryScraping then do
73 it "have valid response" $ do
74 withManager rpcOpts def $ \ mgr -> do
75 xs <- scrape mgr trackerURI [def]
76 L.length xs `shouldSatisfy` (>= 1)
77 else do
78 it "should throw exception" $ do
79 pending
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/SessionSpec.hs b/bittorrent/tests/Network/BitTorrent/Tracker/SessionSpec.hs
new file mode 100644
index 00000000..72936ee7
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/SessionSpec.hs
@@ -0,0 +1,61 @@
1module Network.BitTorrent.Tracker.SessionSpec (spec) where
2import Control.Monad
3import Data.Default
4import Data.List as L
5import Test.Hspec
6
7import Data.Torrent
8import Network.BitTorrent.Tracker.Message
9import Network.BitTorrent.Tracker.List
10import Network.BitTorrent.Tracker.RPC
11import Network.BitTorrent.Tracker.Session
12
13import Config
14
15testSession :: Bool -> (Manager -> Session -> IO ()) -> IO ()
16testSession runEmpty action = do
17 t <- getTestTorrent
18 withManager def def $ \ m -> do
19 withSession m (idInfoHash (tInfoDict t)) (trackerList t) $ \ s ->
20 action m s
21
22 when runEmpty $ do
23 withSession m (idInfoHash (tInfoDict t)) def $ \ s ->
24 action m s
25
26spec :: Spec
27spec = do
28 describe "Session" $ do
29 it "start new session in paused state" $ do
30 testSession True $ \ _ s -> do
31 status <- getStatus s
32 status `shouldBe` Paused
33
34 describe "Query" $ do
35 it "change status after notify" $ do
36 testSession True $ \ m s -> do
37 notify m s Started
38 status <- getStatus s
39 status `shouldBe` Running
40
41 notify m s Stopped
42 stopped <- getStatus s
43 stopped `shouldBe` Paused
44
45 it "completed event do not change status" $ do
46 testSession True $ \ m s -> do
47 notify m s Completed
48 status <- getStatus s
49 status `shouldBe` Paused
50
51 testSession True $ \ m s -> do
52 notify m s Started
53 notify m s Completed
54 status <- getStatus s
55 status `shouldBe` Running
56
57 it "return non-empty list of peers" $ do
58 testSession False $ \ m s -> do
59 notify m s Started
60 peers <- askPeers m s
61 peers `shouldSatisfy` (not . L.null)
diff --git a/bittorrent/tests/Network/BitTorrent/Tracker/TestData.hs b/bittorrent/tests/Network/BitTorrent/Tracker/TestData.hs
new file mode 100644
index 00000000..b95e2df4
--- /dev/null
+++ b/bittorrent/tests/Network/BitTorrent/Tracker/TestData.hs
@@ -0,0 +1,93 @@
1{-# LANGUAGE RecordWildCards #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.BitTorrent.Tracker.TestData
4 ( TrackerEntry (..)
5 , isUdpTracker
6 , isHttpTracker
7 , trackers
8 , badTracker
9 ) where
10
11import Data.Maybe
12import Data.String
13import Network.URI
14
15import Data.Torrent
16
17
18data TrackerEntry = TrackerEntry
19 { -- | May be used to show tracker name in test suite report.
20 trackerName :: String
21
22 -- | Announce uri of the tracker.
23 , trackerURI :: URI
24
25 -- | Some trackers abadoned, so don't even try to announce.
26 , tryAnnounce :: Bool
27
28 -- | Some trackers do not support scraping, so we should not even
29 -- try to scrape them.
30 , tryScraping :: Bool
31
32 -- | Some trackers allow
33 , hashList :: Maybe [InfoHash]
34 }
35
36isUdpTracker :: TrackerEntry -> Bool
37isUdpTracker TrackerEntry {..} = uriScheme trackerURI == "udp:"
38
39isHttpTracker :: TrackerEntry -> Bool
40isHttpTracker TrackerEntry {..} = uriScheme trackerURI == "http:"
41 || uriScheme trackerURI == "https:"
42
43instance IsString URI where
44 fromString str = fromMaybe err $ parseURI str
45 where
46 err = error $ "fromString: bad URI " ++ show str
47
48trackerEntry :: URI -> TrackerEntry
49trackerEntry uri = TrackerEntry
50 { trackerName = maybe "<unknown>" uriRegName (uriAuthority uri)
51 , trackerURI = uri
52 , tryAnnounce = False
53 , tryScraping = False
54 , hashList = Nothing
55 }
56
57announceOnly :: String -> URI -> TrackerEntry
58announceOnly name uri = (trackerEntry uri)
59 { trackerName = name
60 , tryAnnounce = True
61 }
62
63announceScrape :: String -> URI -> TrackerEntry
64announceScrape name uri = (announceOnly name uri)
65 { tryScraping = True
66 }
67
68notWorking :: String -> URI -> TrackerEntry
69notWorking name uri = (trackerEntry uri)
70 { trackerName = name
71 }
72
73trackers :: [TrackerEntry]
74trackers =
75 [ (announceOnly "LinuxTracker"
76 "http://linuxtracker.org:2710/00000000000000000000000000000000/announce")
77 { hashList = Just ["1c82a95b9e02bf3db4183da072ad3ef656aacf0e"] -- debian 7
78 }
79
80 , (announceScrape "Arch" "http://tracker.archlinux.org:6969/announce")
81 { hashList = Just ["bc9ae647a3e6c3636de58535dd3f6360ce9f4621"]
82 }
83
84 , notWorking "rarbg" "udp://9.rarbg.com:2710/announce"
85
86 , announceScrape "OpenBitTorrent" "udp://tracker.openbittorrent.com:80/announce"
87 , announceScrape "PublicBT" "udp://tracker.publicbt.com:80/announce"
88 , notWorking "OpenBitTorrent" "http://tracker.openbittorrent.com:80/announce"
89 , notWorking "PublicBT" "http://tracker.publicbt.com:80/announce"
90 ]
91
92badTracker :: TrackerEntry
93badTracker = notWorking "rarbg" "udp://9.rarbg.com:2710/announce" \ No newline at end of file
diff --git a/bittorrent/tests/Network/KRPC/MessageSpec.hs b/bittorrent/tests/Network/KRPC/MessageSpec.hs
new file mode 100644
index 00000000..498ef679
--- /dev/null
+++ b/bittorrent/tests/Network/KRPC/MessageSpec.hs
@@ -0,0 +1,72 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.KRPC.MessageSpec (spec) where
4import Control.Applicative
5import Data.ByteString.Lazy as BL
6import Test.Hspec
7import Test.QuickCheck
8import Test.QuickCheck.Instances ()
9
10import Data.BEncode as BE
11import Network.KRPC.Message
12
13instance Arbitrary ErrorCode where
14 arbitrary = arbitraryBoundedEnum
15
16instance Arbitrary KError where
17 arbitrary = KError <$> arbitrary <*> arbitrary <*> arbitrary
18
19instance Arbitrary KQuery where
20 arbitrary = KQuery <$> pure (BInteger 0) <*> arbitrary <*> arbitrary
21
22instance Arbitrary KResponse where
23 -- TODO: Abitrary instance for ReflectedIP
24 arbitrary = KResponse <$> pure (BList []) <*> arbitrary <*> pure Nothing
25
26instance Arbitrary KMessage where
27 arbitrary = frequency
28 [ (1, Q <$> arbitrary)
29 , (1, R <$> arbitrary)
30 , (1, E <$> arbitrary)
31 ]
32
33spec :: Spec
34spec = do
35 describe "error message" $ do
36 it "properly bencoded (iso)" $ property $ \ ke ->
37 BE.decode (BL.toStrict (BE.encode ke)) `shouldBe` Right (ke :: KError)
38
39 it "properly bencoded" $ do
40 BE.decode "d1:eli201e23:A Generic Error Ocurrede1:t2:aa1:y1:ee"
41 `shouldBe` Right (KError GenericError "A Generic Error Ocurred" "aa")
42
43 BE.decode "d1:eli202e22:A Server Error Ocurrede1:t2:bb1:y1:ee"
44 `shouldBe` Right (KError ServerError "A Server Error Ocurred" "bb")
45
46 BE.decode "d1:eli203e24:A Protocol Error Ocurrede1:t2:cc1:y1:ee"
47 `shouldBe` Right (KError ProtocolError "A Protocol Error Ocurred" "cc")
48
49 BE.decode "d1:eli204e30:Attempt to call unknown methode1:t2:dd1:y1:ee"
50 `shouldBe` Right
51 (KError MethodUnknown "Attempt to call unknown method" "dd")
52
53 describe "query message" $ do
54 it "properly bencoded (iso)" $ property $ \ kq ->
55 BE.decode (BL.toStrict (BE.encode kq)) `shouldBe` Right (kq :: KQuery)
56
57 it "properly bencoded" $ do
58 BE.decode "d1:ale1:q4:ping1:t2:aa1:y1:qe" `shouldBe`
59 Right (KQuery (BList []) "ping" "aa")
60
61
62 describe "response message" $ do
63 it "properly bencoded (iso)" $ property $ \ kr ->
64 BE.decode (BL.toStrict (BE.encode kr)) `shouldBe` Right (kr :: KResponse)
65
66 it "properly bencoded" $ do
67 BE.decode "d1:rle1:t2:aa1:y1:re" `shouldBe`
68 Right (KResponse (BList []) "aa" Nothing)
69
70 describe "generic message" $ do
71 it "properly bencoded (iso)" $ property $ \ km ->
72 BE.decode (BL.toStrict (BE.encode km)) `shouldBe` Right (km :: KMessage)
diff --git a/bittorrent/tests/Network/KRPC/MethodSpec.hs b/bittorrent/tests/Network/KRPC/MethodSpec.hs
new file mode 100644
index 00000000..c1c58282
--- /dev/null
+++ b/bittorrent/tests/Network/KRPC/MethodSpec.hs
@@ -0,0 +1,52 @@
1{-# LANGUAGE MultiParamTypeClasses #-}
2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5{-# LANGUAGE DeriveDataTypeable #-}
6{-# OPTIONS_GHC -fno-warn-orphans #-}
7module Network.KRPC.MethodSpec where
8import Control.Applicative
9import Data.BEncode
10import Data.ByteString as BS
11import Data.Typeable
12import Network.KRPC
13import Test.Hspec
14
15
16data Ping = Ping
17 deriving (Show, Eq, Typeable)
18
19instance BEncode Ping where
20 toBEncode Ping = toBEncode ()
21 fromBEncode b = Ping <$ (fromBEncode b :: Result ())
22
23instance KRPC Ping Ping
24
25ping :: Monad h => Handler h
26ping = handler $ \ _ Ping -> return Ping
27
28newtype Echo a = Echo a
29 deriving (Show, Eq, BEncode, Typeable)
30
31echo :: Monad h => Handler h
32echo = handler $ \ _ (Echo a) -> return (Echo (a :: ByteString))
33
34instance (Typeable a, BEncode a) => KRPC (Echo a) (Echo a)
35
36spec :: Spec
37spec = do
38 describe "ping method" $ do
39 it "name is ping" $ do
40 (method :: Method Ping Ping) `shouldBe` "ping"
41
42 it "has pretty Show instance" $ do
43 show (method :: Method Ping Ping) `shouldBe` "ping :: Ping -> Ping"
44
45 describe "echo method" $ do
46 it "is overloadable" $ do
47 (method :: Method (Echo Int ) (Echo Int )) `shouldBe` "echo int"
48 (method :: Method (Echo Bool) (Echo Bool)) `shouldBe` "echo bool"
49
50 it "has pretty Show instance" $ do
51 show (method :: Method (Echo Int) (Echo Int))
52 `shouldBe` "echo int :: Echo Int -> Echo Int" \ No newline at end of file
diff --git a/bittorrent/tests/Network/KRPCSpec.hs b/bittorrent/tests/Network/KRPCSpec.hs
new file mode 100644
index 00000000..eabcc817
--- /dev/null
+++ b/bittorrent/tests/Network/KRPCSpec.hs
@@ -0,0 +1,59 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3module Network.KRPCSpec (spec) where
4import Control.Monad.Logger
5import Control.Monad.Reader
6import Network.KRPC
7import Network.KRPC.MethodSpec hiding (spec)
8import Test.Hspec
9
10servAddr :: SockAddr
11servAddr = SockAddrInet 6000 (256 * 256 * 256 + 127)
12
13handlers :: [Handler IO]
14handlers =
15 [ handler $ \ _ Ping -> return Ping
16 , handler $ \ _ (Echo a) -> return (Echo (a :: Bool))
17 , handler $ \ _ (Echo a) -> return (Echo (a :: Int))
18 ]
19
20instance MonadLogger IO where
21 monadLoggerLog _ _ _ _ = return ()
22
23opts :: Options
24opts = def { optQueryTimeout = 1 }
25
26spec :: Spec
27spec = do
28 let qr :: MonadKRPC h m => SockAddr -> Echo Int -> m (Echo Int)
29 qr = query
30
31 describe "manager" $ do
32 it "is active until closeManager called" $ do
33 m <- newManager opts servAddr []
34 isActive m `shouldReturn` True
35 closeManager m
36 isActive m `shouldReturn` False
37
38 describe "query" $ do
39 it "run handlers" $ do
40 let int = 0xabcd :: Int
41 (withManager opts servAddr handlers $ runReaderT $ do
42 listen
43 query servAddr (Echo int))
44 `shouldReturn` Echo int
45
46 it "count transactions properly" $ do
47 (withManager opts servAddr handlers $ runReaderT $ do
48 listen
49 _ <- qr servAddr (Echo 0xabcd)
50 _ <- qr servAddr (Echo 0xabcd)
51 getQueryCount
52 )
53 `shouldReturn` 2
54
55 it "throw timeout exception" $ do
56 (withManager opts servAddr handlers $ runReaderT $ do
57 qr servAddr (Echo 0xabcd)
58 )
59 `shouldThrow` (== TimeoutExpired)
diff --git a/bittorrent/tests/Readme.md b/bittorrent/tests/Readme.md
new file mode 100644
index 00000000..7a9d8914
--- /dev/null
+++ b/bittorrent/tests/Readme.md
@@ -0,0 +1,4 @@
1Prerequisites
2=============
3
4To run test suite you need rtorrent and screen installed.
diff --git a/bittorrent/tests/Spec.hs b/bittorrent/tests/Spec.hs
new file mode 100644
index 00000000..b4e92e75
--- /dev/null
+++ b/bittorrent/tests/Spec.hs
@@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-}
diff --git a/bittorrent/tests/System/Torrent/FileMapSpec.hs b/bittorrent/tests/System/Torrent/FileMapSpec.hs
new file mode 100644
index 00000000..29252925
--- /dev/null
+++ b/bittorrent/tests/System/Torrent/FileMapSpec.hs
@@ -0,0 +1,116 @@
1-- this is test string used in the 'spec' --- don't touch me!
2module System.Torrent.FileMapSpec (spec) where
3
4import Control.Monad.Loops
5import Data.List as L
6import Data.ByteString.Lazy as BL
7import System.Directory
8import System.FilePath
9import System.IO.Temp
10import Test.Hspec
11
12import Data.Torrent
13import System.Torrent.FileMap as FM
14
15
16withLayout :: (FileLayout FileSize -> IO ()) -> IO ()
17withLayout f = do
18 tmp <- getTemporaryDirectory
19 withTempDirectory tmp "bittorrentTestDir" $ \dir ->
20 f [ (dir </> "a", 2)
21 , (dir </> "b", 3)
22 , (dir </> "c", 2)
23 ] `seq` return ()
24
25spec :: Spec
26spec = do
27 describe "mmapFiles" $ do
28 it "creates new files" $ withLayout $ \layout -> do
29 m <- mmapFiles ReadWriteEx layout
30 unmapFiles m
31
32 (doesFileExist . fst) `allM` layout
33 `shouldReturn` True
34
35 describe "size" $ do
36 it "is equal to the layout size" $ withLayout $ \layout -> do
37 m <- mmapFiles ReadOnly layout
38 FM.size m `shouldBe` L.sum (L.map snd layout)
39 unmapFiles m
40
41 describe "readBytes" $ do
42 it "read from files" $ do
43 let thisFile = [("tests/System/Torrent/FileMapSpec.hs", 15)]
44 m <- mmapFiles ReadOnly thisFile
45 readBytes 3 15 m `shouldReturn` "this is test"
46 unmapFiles m
47
48 it "ignore underflow reads" $ withLayout $ \layout -> do
49 m <- mmapFiles ReadOnly layout
50 readBytes (-1) 1 m `shouldReturn` ""
51 readBytes (-5) 12 m `shouldReturn` ""
52 unmapFiles m
53
54 it "crop overflow reads" $ withLayout $ \layout -> do
55 _m <- mmapFiles ReadWrite layout
56 writeBytes 5 "cc" _m
57 unmapFiles _m
58
59 m <- mmapFiles ReadOnly layout
60 readBytes 5 10 m `shouldReturn` "cc"
61 unmapFiles m
62
63 describe "writeBytes" $ do
64 it "writes to files" $ withLayout $ \layout -> do
65 m <- mmapFiles ReadWriteEx layout
66 writeBytes 0 "a" m
67 readBytes 0 1 m `shouldReturn` "a"
68 writeBytes 1 "ab" m
69 readBytes 1 2 m `shouldReturn` "ab"
70 writeBytes 3 "b" m
71 readBytes 3 1 m `shouldReturn` "b"
72 writeBytes 4 "bc" m
73 readBytes 4 2 m `shouldReturn` "bc"
74 writeBytes 6 "c" m
75 readBytes 6 1 m `shouldReturn` "c"
76 readBytes 0 7 m `shouldReturn` "aabbbcc"
77 unmapFiles m
78
79 BL.readFile (fst (layout !! 0)) `shouldReturn` "aa"
80 BL.readFile (fst (layout !! 1)) `shouldReturn` "bbb"
81 BL.readFile (fst (layout !! 2)) `shouldReturn` "cc"
82
83 let max_page_size = 4 * 1024 * 1024
84 let long_bs = BL.replicate (fromIntegral max_page_size) 0
85
86 it "no buffer underflow errors" $ withLayout $ \layout -> do
87 m <- mmapFiles ReadWrite layout
88 writeBytes (1 - max_page_size) long_bs m
89 unmapFiles m
90
91 it "no buffer overflow errors" $ withLayout $ \layout -> do
92 m <- mmapFiles ReadWrite layout
93 writeBytes 5 long_bs m
94 unmapFiles m
95
96 it "ignore underflow writes" $ withLayout $ \layout -> do
97 _m <- mmapFiles ReadWrite layout
98 writeBytes 0 "aa" _m
99 unmapFiles _m
100
101 m <- mmapFiles ReadWrite layout
102 writeBytes (-1) "hhh" m
103 unmapFiles m
104 BL.readFile (fst (layout !! 0)) `shouldReturn` "aa"
105
106 it "crop overflow writes" $ withLayout $ \layout -> do
107 m <- mmapFiles ReadWrite layout
108 writeBytes 5 "ddddddddd" m
109 unmapFiles m
110 BL.readFile (fst (layout !! 2)) `shouldReturn` "dd"
111
112 describe "from/to lazy bytestring" $ do
113 it "isomorphic to lazy bytestring" $ withLayout $ \layout -> do
114 m <- mmapFiles ReadOnly layout
115 fromLazyByteString (toLazyByteString m) `shouldBe` m
116 unmapFiles m
diff --git a/bittorrent/tests/System/Torrent/StorageSpec.hs b/bittorrent/tests/System/Torrent/StorageSpec.hs
new file mode 100644
index 00000000..b5e49078
--- /dev/null
+++ b/bittorrent/tests/System/Torrent/StorageSpec.hs
@@ -0,0 +1,91 @@
1module System.Torrent.StorageSpec (spec) where
2import Data.ByteString.Lazy as BL
3import Data.Conduit as C
4import Data.Conduit.List as C
5import System.FilePath
6import System.Directory
7import System.IO.Unsafe
8import Test.Hspec
9
10import Data.Torrent
11import Network.BitTorrent.Exchange.Bitfield as BF
12import System.Torrent.Storage
13
14
15layout :: FileLayout FileSize
16layout =
17 [ (dir </> "_a", 20)
18 , (dir </> "_b", 50)
19 , (dir </> "_c", 100)
20 , (dir </> "_d", 5)
21 ]
22 where
23 dir = unsafePerformIO $ getTemporaryDirectory
24
25createLayout :: IO ()
26createLayout = withStorage ReadWriteEx 1 layout (const (return ()))
27
28psize :: PieceSize
29psize = 16
30
31pcount :: PieceCount
32pcount = 11
33
34spec :: Spec
35spec = before createLayout $ do
36 describe "writePiece" $ do
37 it "should fail gracefully on write operation in RO mode" $ do
38 withStorage ReadOnly 1 layout $ \ s ->
39 writePiece (Piece 0 "a") s `shouldThrow` (== StorageIsRO)
40
41 it "should fail if piece size do not match" $ do
42 withStorage ReadWrite 1 layout $ \ s ->
43 writePiece (Piece 0 "") s `shouldThrow` (== InvalidSize 0)
44
45 it "should fail on negative index" $ do
46 withStorage ReadWrite 1 layout $ \ s ->
47 writePiece (Piece (-1) "") s `shouldThrow` (== InvalidIndex (-1))
48
49 it "should fail on out of upper bound index" $ do
50 withStorage ReadWrite 100 layout $ \ s -> do
51 let bs = BL.replicate 100 0
52 writePiece (Piece 0 bs) s
53
54 let bs' = BL.replicate 75 0
55 writePiece (Piece 1 bs') s
56
57 writePiece (Piece 2 bs') s `shouldThrow` (== InvalidIndex 2)
58
59 describe "readPiece" $ do
60 it "should fail on negative index" $
61 withStorage ReadOnly 1 layout $ \ s ->
62 readPiece (-1) s `shouldThrow` (== InvalidIndex (-1))
63
64 it "should fail on out of upper bound index" $ do
65 withStorage ReadOnly 100 layout $ \ s -> do
66 _ <- readPiece 1 s
67 readPiece 2 s `shouldThrow` (== InvalidIndex 2)
68
69 describe "sourceStorage" $ do
70 it "should source all chunks" $ do
71 withStorage ReadOnly psize layout $ \ s -> do
72 n <- sourceStorage s $$ C.fold (\ n _ -> succ n) 0
73 n `shouldBe` pcount
74
75 -- this test should fail if 'sourceStorage' test fail
76 describe "sinkStorage" $ do
77 it "should write all chunks" $ do
78 let byteVal = 0
79 let bzeroPiece p = p { pieceData = BL.replicate (BL.length (pieceData p)) byteVal }
80 let isZeroPiece p = (== byteVal) `BL.all` pieceData p
81
82 withStorage ReadWrite psize layout $ \ s -> do
83 sourceStorage s $= C.map bzeroPiece $$ sinkStorage s
84 b <- sourceStorage s $$ C.fold (\ b p -> b && isZeroPiece p) True
85 b `shouldBe` True
86
87 describe "genPieceInfo" $ do
88 it "" $ do
89 withStorage ReadWrite psize layout $ \ s -> do
90 bf <- genPieceInfo s >>= getBitfield s
91 bf `shouldSatisfy` BF.full \ No newline at end of file
diff --git a/c b/c
new file mode 100755
index 00000000..ed905d3d
--- /dev/null
+++ b/c
@@ -0,0 +1,8 @@
1#!/bin/sh
2compile=ghc
3defs="-DBENCODE_AESON -DTHREAD_DEBUG"
4hide="-hide-package crypto-random -hide-package crypto-api -hide-package crypto-numbers -hide-package cryptohash -hide-package prettyclass"
5cbits="cbits/*.c"
6# -Wno-typed-holes
7includes="-isrc -icryptonite-backport"
8$compile -Wmissing-signatures -fdefer-typed-holes -freverse-errors $hide $includes -XOverloadedStrings -XRecordWildCards $defs $cbits "$@"
diff --git a/cbits/cryptonite_bitfn.h b/cbits/cryptonite_bitfn.h
new file mode 100644
index 00000000..3a00dd8a
--- /dev/null
+++ b/cbits/cryptonite_bitfn.h
@@ -0,0 +1,253 @@
1/*
2 * Copyright (C) 2006-2014 Vincent Hanquez <vincent@snarc.org>
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
14 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
15 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
16 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
17 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
18 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
19 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
20 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
22 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 */
24
25#ifndef BITFN_H
26#define BITFN_H
27#include <stdint.h>
28
29#ifndef NO_INLINE_ASM
30/**********************************************************/
31# if (defined(__i386__))
32# define ARCH_HAS_SWAP32
33static inline uint32_t bitfn_swap32(uint32_t a)
34{
35 asm ("bswap %0" : "=r" (a) : "0" (a));
36 return a;
37}
38/**********************************************************/
39# elif (defined(__arm__))
40# define ARCH_HAS_SWAP32
41static inline uint32_t bitfn_swap32(uint32_t a)
42{
43 uint32_t tmp = a;
44 asm volatile ("eor %1, %0, %0, ror #16\n"
45 "bic %1, %1, #0xff0000\n"
46 "mov %0, %0, ror #8\n"
47 "eor %0, %0, %1, lsr #8\n"
48 : "=r" (a), "=r" (tmp) : "0" (a), "1" (tmp));
49 return a;
50}
51/**********************************************************/
52# elif defined(__x86_64__)
53# define ARCH_HAS_SWAP32
54# define ARCH_HAS_SWAP64
55static inline uint32_t bitfn_swap32(uint32_t a)
56{
57 asm ("bswap %0" : "=r" (a) : "0" (a));
58 return a;
59}
60
61static inline uint64_t bitfn_swap64(uint64_t a)
62{
63 asm ("bswap %0" : "=r" (a) : "0" (a));
64 return a;
65}
66
67# endif
68#endif /* NO_INLINE_ASM */
69/**********************************************************/
70
71#ifndef ARCH_HAS_ROL32
72static inline uint32_t rol32(uint32_t word, uint32_t shift)
73{
74 return (word << shift) | (word >> (32 - shift));
75}
76#endif
77
78#ifndef ARCH_HAS_ROR32
79static inline uint32_t ror32(uint32_t word, uint32_t shift)
80{
81 return (word >> shift) | (word << (32 - shift));
82}
83#endif
84
85#ifndef ARCH_HAS_ROL64
86static inline uint64_t rol64(uint64_t word, uint32_t shift)
87{
88 return (word << shift) | (word >> (64 - shift));
89}
90#endif
91
92#ifndef ARCH_HAS_ROR64
93static inline uint64_t ror64(uint64_t word, uint32_t shift)
94{
95 return (word >> shift) | (word << (64 - shift));
96}
97#endif
98
99#ifndef ARCH_HAS_SWAP32
100static inline uint32_t bitfn_swap32(uint32_t a)
101{
102 return (a << 24) | ((a & 0xff00) << 8) | ((a >> 8) & 0xff00) | (a >> 24);
103}
104#endif
105
106#ifndef ARCH_HAS_ARRAY_SWAP32
107static inline void array_swap32(uint32_t *d, uint32_t *s, uint32_t nb)
108{
109 while (nb--)
110 *d++ = bitfn_swap32(*s++);
111}
112#endif
113
114#ifndef ARCH_HAS_SWAP64
115static inline uint64_t bitfn_swap64(uint64_t a)
116{
117 return ((uint64_t) bitfn_swap32((uint32_t) (a >> 32))) |
118 (((uint64_t) bitfn_swap32((uint32_t) a)) << 32);
119}
120#endif
121
122#ifndef ARCH_HAS_ARRAY_SWAP64
123static inline void array_swap64(uint64_t *d, uint64_t *s, uint32_t nb)
124{
125 while (nb--)
126 *d++ = bitfn_swap64(*s++);
127}
128#endif
129
130#ifndef ARCH_HAS_MEMORY_ZERO
131static inline void memory_zero(void *ptr, uint32_t len)
132{
133 uint32_t *ptr32 = ptr;
134 uint8_t *ptr8;
135 int i;
136
137 for (i = 0; i < len / 4; i++)
138 *ptr32++ = 0;
139 if (len % 4) {
140 ptr8 = (uint8_t *) ptr32;
141 for (i = len % 4; i >= 0; i--)
142 ptr8[i] = 0;
143 }
144}
145#endif
146
147#ifndef ARCH_HAS_ARRAY_COPY32
148static inline void array_copy32(uint32_t *d, uint32_t *s, uint32_t nb)
149{
150 while (nb--) *d++ = *s++;
151}
152#endif
153
154#ifndef ARCH_HAS_ARRAY_XOR32
155static inline void array_xor32(uint32_t *d, uint32_t *s, uint32_t nb)
156{
157 while (nb--) *d++ ^= *s++;
158}
159#endif
160
161#ifndef ARCH_HAS_ARRAY_COPY64
162static inline void array_copy64(uint64_t *d, uint64_t *s, uint32_t nb)
163{
164 while (nb--) *d++ = *s++;
165}
166#endif
167
168#ifdef __GNUC__
169#define bitfn_ntz(n) __builtin_ctz(n)
170#else
171#error "define ntz for your platform"
172#endif
173
174#ifdef __MINGW32__
175 # define LITTLE_ENDIAN 1234
176 # define BYTE_ORDER LITTLE_ENDIAN
177#elif defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__)
178 # include <sys/endian.h>
179#elif defined(__OpenBSD__) || defined(__SVR4)
180 # include <sys/types.h>
181#elif defined(__APPLE__)
182 # include <machine/endian.h>
183#elif defined( BSD ) && ( BSD >= 199103 )
184 # include <machine/endian.h>
185#elif defined( __QNXNTO__ ) && defined( __LITTLEENDIAN__ )
186 # define LITTLE_ENDIAN 1234
187 # define BYTE_ORDER LITTLE_ENDIAN
188#elif defined( __QNXNTO__ ) && defined( __BIGENDIAN__ )
189 # define BIG_ENDIAN 1234
190 # define BYTE_ORDER BIG_ENDIAN
191#elif defined( _AIX )
192 # include <sys/machine.h>
193#else
194 # include <endian.h>
195#endif
196/* big endian to cpu */
197#if LITTLE_ENDIAN == BYTE_ORDER
198
199# define be32_to_cpu(a) bitfn_swap32(a)
200# define cpu_to_be32(a) bitfn_swap32(a)
201# define le32_to_cpu(a) (a)
202# define cpu_to_le32(a) (a)
203# define be64_to_cpu(a) bitfn_swap64(a)
204# define cpu_to_be64(a) bitfn_swap64(a)
205# define le64_to_cpu(a) (a)
206# define cpu_to_le64(a) (a)
207
208# define cpu_to_le32_array(d, s, l) array_copy32(d, s, l)
209# define le32_to_cpu_array(d, s, l) array_copy32(d, s, l)
210# define cpu_to_be32_array(d, s, l) array_swap32(d, s, l)
211# define be32_to_cpu_array(d, s, l) array_swap32(d, s, l)
212
213# define cpu_to_le64_array(d, s, l) array_copy64(d, s, l)
214# define le64_to_cpu_array(d, s, l) array_copy64(d, s, l)
215# define cpu_to_be64_array(d, s, l) array_swap64(d, s, l)
216# define be64_to_cpu_array(d, s, l) array_swap64(d, s, l)
217
218# define ror32_be(a, s) rol32(a, s)
219# define rol32_be(a, s) ror32(a, s)
220
221# define ARCH_IS_LITTLE_ENDIAN
222
223#elif BIG_ENDIAN == BYTE_ORDER
224
225# define be32_to_cpu(a) (a)
226# define cpu_to_be32(a) (a)
227# define be64_to_cpu(a) (a)
228# define cpu_to_be64(a) (a)
229# define le64_to_cpu(a) bitfn_swap64(a)
230# define cpu_to_le64(a) bitfn_swap64(a)
231# define le32_to_cpu(a) bitfn_swap32(a)
232# define cpu_to_le32(a) bitfn_swap32(a)
233
234# define cpu_to_le32_array(d, s, l) array_swap32(d, s, l)
235# define le32_to_cpu_array(d, s, l) array_swap32(d, s, l)
236# define cpu_to_be32_array(d, s, l) array_copy32(d, s, l)
237# define be32_to_cpu_array(d, s, l) array_copy32(d, s, l)
238
239# define cpu_to_le64_array(d, s, l) array_swap64(d, s, l)
240# define le64_to_cpu_array(d, s, l) array_swap64(d, s, l)
241# define cpu_to_be64_array(d, s, l) array_copy64(d, s, l)
242# define be64_to_cpu_array(d, s, l) array_copy64(d, s, l)
243
244# define ror32_be(a, s) ror32(a, s)
245# define rol32_be(a, s) rol32(a, s)
246
247# define ARCH_IS_BIG_ENDIAN
248
249#else
250# error "endian not supported"
251#endif
252
253#endif /* !BITFN_H */
diff --git a/cbits/cryptonite_salsa.c b/cbits/cryptonite_salsa.c
new file mode 100644
index 00000000..0bd96607
--- /dev/null
+++ b/cbits/cryptonite_salsa.c
@@ -0,0 +1,297 @@
1/*
2 * Copyright (c) 2014-2015 Vincent Hanquez <vincent@snarc.org>
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the author nor the names of his contributors
15 * may be used to endorse or promote products derived from this software
16 * without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 */
30
31#include <stdint.h>
32#include <string.h>
33#include <stdio.h>
34#include "cryptonite_salsa.h"
35#include "cryptonite_bitfn.h"
36
37static const uint8_t sigma[16] = "expand 32-byte k";
38static const uint8_t tau[16] = "expand 16-byte k";
39
40#define QR(a,b,c,d) \
41 b ^= rol32(a+d, 7); \
42 c ^= rol32(b+a, 9); \
43 d ^= rol32(c+b, 13); \
44 a ^= rol32(d+c, 18);
45
46#define ALIGNED64(PTR) \
47 (((uintptr_t)(const void *)(PTR)) % 8 == 0)
48
49#define SALSA_CORE_LOOP \
50 for (i = rounds; i > 0; i -= 2) { \
51 QR (x0,x4,x8,x12); \
52 QR (x5,x9,x13,x1); \
53 QR (x10,x14,x2,x6); \
54 QR (x15,x3,x7,x11); \
55 QR (x0,x1,x2,x3); \
56 QR (x5,x6,x7,x4); \
57 QR (x10,x11,x8,x9); \
58 QR (x15,x12,x13,x14); \
59 }
60
61static inline uint32_t load32(const uint8_t *p)
62{
63 return le32_to_cpu(*((uint32_t *) p));
64}
65
66static void salsa_core(int rounds, block *out, const cryptonite_salsa_state *in)
67{
68 uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
69 int i;
70
71 x0 = in->d[0]; x1 = in->d[1]; x2 = in->d[2]; x3 = in->d[3];
72 x4 = in->d[4]; x5 = in->d[5]; x6 = in->d[6]; x7 = in->d[7];
73 x8 = in->d[8]; x9 = in->d[9]; x10 = in->d[10]; x11 = in->d[11];
74 x12 = in->d[12]; x13 = in->d[13]; x14 = in->d[14]; x15 = in->d[15];
75
76 SALSA_CORE_LOOP;
77
78 x0 += in->d[0]; x1 += in->d[1]; x2 += in->d[2]; x3 += in->d[3];
79 x4 += in->d[4]; x5 += in->d[5]; x6 += in->d[6]; x7 += in->d[7];
80 x8 += in->d[8]; x9 += in->d[9]; x10 += in->d[10]; x11 += in->d[11];
81 x12 += in->d[12]; x13 += in->d[13]; x14 += in->d[14]; x15 += in->d[15];
82
83 out->d[0] = cpu_to_le32(x0);
84 out->d[1] = cpu_to_le32(x1);
85 out->d[2] = cpu_to_le32(x2);
86 out->d[3] = cpu_to_le32(x3);
87 out->d[4] = cpu_to_le32(x4);
88 out->d[5] = cpu_to_le32(x5);
89 out->d[6] = cpu_to_le32(x6);
90 out->d[7] = cpu_to_le32(x7);
91 out->d[8] = cpu_to_le32(x8);
92 out->d[9] = cpu_to_le32(x9);
93 out->d[10] = cpu_to_le32(x10);
94 out->d[11] = cpu_to_le32(x11);
95 out->d[12] = cpu_to_le32(x12);
96 out->d[13] = cpu_to_le32(x13);
97 out->d[14] = cpu_to_le32(x14);
98 out->d[15] = cpu_to_le32(x15);
99}
100
101void cryptonite_salsa_core_xor(int rounds, block *out, block *in)
102{
103 uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
104 int i;
105
106#define LOAD(i) (out->d[i] ^= in->d[i])
107 x0 = LOAD(0); x1 = LOAD(1); x2 = LOAD(2); x3 = LOAD(3);
108 x4 = LOAD(4); x5 = LOAD(5); x6 = LOAD(6); x7 = LOAD(7);
109 x8 = LOAD(8); x9 = LOAD(9); x10 = LOAD(10); x11 = LOAD(11);
110 x12 = LOAD(12); x13 = LOAD(13); x14 = LOAD(14); x15 = LOAD(15);
111#undef LOAD
112
113 SALSA_CORE_LOOP;
114
115 out->d[0] += x0; out->d[1] += x1; out->d[2] += x2; out->d[3] += x3;
116 out->d[4] += x4; out->d[5] += x5; out->d[6] += x6; out->d[7] += x7;
117 out->d[8] += x8; out->d[9] += x9; out->d[10] += x10; out->d[11] += x11;
118 out->d[12] += x12; out->d[13] += x13; out->d[14] += x14; out->d[15] += x15;
119}
120
121/* only 2 valid values for keylen are 256 (32) and 128 (16) */
122void cryptonite_salsa_init_core(cryptonite_salsa_state *st,
123 uint32_t keylen, const uint8_t *key,
124 uint32_t ivlen, const uint8_t *iv)
125{
126 const uint8_t *constants = (keylen == 32) ? sigma : tau;
127 int i;
128
129 st->d[0] = load32(constants + 0);
130 st->d[5] = load32(constants + 4);
131 st->d[10] = load32(constants + 8);
132 st->d[15] = load32(constants + 12);
133
134 st->d[1] = load32(key + 0);
135 st->d[2] = load32(key + 4);
136 st->d[3] = load32(key + 8);
137 st->d[4] = load32(key + 12);
138 /* we repeat the key on 128 bits */
139 if (keylen == 32)
140 key += 16;
141 st->d[11] = load32(key + 0);
142 st->d[12] = load32(key + 4);
143 st->d[13] = load32(key + 8);
144 st->d[14] = load32(key + 12);
145
146 st->d[9] = 0;
147 switch (ivlen) {
148 case 8:
149 st->d[6] = load32(iv + 0);
150 st->d[7] = load32(iv + 4);
151 st->d[8] = 0;
152 break;
153 case 12:
154 st->d[6] = load32(iv + 0);
155 st->d[7] = load32(iv + 4);
156 st->d[8] = load32(iv + 8);
157 default:
158 return;
159 }
160}
161
162void cryptonite_salsa_init(cryptonite_salsa_context *ctx, uint8_t nb_rounds,
163 uint32_t keylen, const uint8_t *key,
164 uint32_t ivlen, const uint8_t *iv)
165{
166 memset(ctx, 0, sizeof(*ctx));
167 ctx->nb_rounds = nb_rounds;
168 cryptonite_salsa_init_core(&ctx->st, keylen, key, ivlen, iv);
169}
170
171void cryptonite_salsa_combine(uint8_t *dst, cryptonite_salsa_context *ctx, const uint8_t *src, uint32_t bytes)
172{
173 block out;
174 cryptonite_salsa_state *st;
175 int i;
176
177 if (!bytes)
178 return;
179
180 /* xor the previous buffer first (if any) */
181 if (ctx->prev_len > 0) {
182 int to_copy = (ctx->prev_len < bytes) ? ctx->prev_len : bytes;
183 for (i = 0; i < to_copy; i++)
184 dst[i] = src[i] ^ ctx->prev[ctx->prev_ofs+i];
185 memset(ctx->prev + ctx->prev_ofs, 0, to_copy);
186 ctx->prev_len -= to_copy;
187 ctx->prev_ofs += to_copy;
188 src += to_copy;
189 dst += to_copy;
190 bytes -= to_copy;
191 }
192
193 if (bytes == 0)
194 return;
195
196 st = &ctx->st;
197
198 /* xor new 64-bytes chunks and store the left over if any */
199 for (; bytes >= 64; bytes -= 64, src += 64, dst += 64) {
200 /* generate new chunk and update state */
201 salsa_core(ctx->nb_rounds, &out, st);
202 st->d[8] += 1;
203 if (st->d[8] == 0)
204 st->d[9] += 1;
205
206 for (i = 0; i < 64; ++i)
207 dst[i] = src[i] ^ out.b[i];
208 }
209
210 if (bytes > 0) {
211 /* generate new chunk and update state */
212 salsa_core(ctx->nb_rounds, &out, st);
213 st->d[8] += 1;
214 if (st->d[8] == 0)
215 st->d[9] += 1;
216
217 /* xor as much as needed */
218 for (i = 0; i < bytes; i++)
219 dst[i] = src[i] ^ out.b[i];
220
221 /* copy the left over in the buffer */
222 ctx->prev_len = 64 - bytes;
223 ctx->prev_ofs = i;
224 for (; i < 64; i++) {
225 ctx->prev[i] = out.b[i];
226 }
227 }
228}
229
230void cryptonite_salsa_generate(uint8_t *dst, cryptonite_salsa_context *ctx, uint32_t bytes)
231{
232 cryptonite_salsa_state *st;
233 block out;
234 int i;
235
236 if (!bytes)
237 return;
238
239 /* xor the previous buffer first (if any) */
240 if (ctx->prev_len > 0) {
241 int to_copy = (ctx->prev_len < bytes) ? ctx->prev_len : bytes;
242 for (i = 0; i < to_copy; i++)
243 dst[i] = ctx->prev[ctx->prev_ofs+i];
244 memset(ctx->prev + ctx->prev_ofs, 0, to_copy);
245 ctx->prev_len -= to_copy;
246 ctx->prev_ofs += to_copy;
247 dst += to_copy;
248 bytes -= to_copy;
249 }
250
251 if (bytes == 0)
252 return;
253
254 st = &ctx->st;
255
256 if (ALIGNED64(dst)) {
257 /* xor new 64-bytes chunks and store the left over if any */
258 for (; bytes >= 64; bytes -= 64, dst += 64) {
259 /* generate new chunk and update state */
260 salsa_core(ctx->nb_rounds, (block *) dst, st);
261 st->d[8] += 1;
262 if (st->d[8] == 0)
263 st->d[9] += 1;
264 }
265 } else {
266 /* xor new 64-bytes chunks and store the left over if any */
267 for (; bytes >= 64; bytes -= 64, dst += 64) {
268 /* generate new chunk and update state */
269 salsa_core(ctx->nb_rounds, &out, st);
270 st->d[8] += 1;
271 if (st->d[8] == 0)
272 st->d[9] += 1;
273
274 for (i = 0; i < 64; ++i)
275 dst[i] = out.b[i];
276 }
277 }
278
279 if (bytes > 0) {
280 /* generate new chunk and update state */
281 salsa_core(ctx->nb_rounds, &out, st);
282 st->d[8] += 1;
283 if (st->d[8] == 0)
284 st->d[9] += 1;
285
286 /* xor as much as needed */
287 for (i = 0; i < bytes; i++)
288 dst[i] = out.b[i];
289
290 /* copy the left over in the buffer */
291 ctx->prev_len = 64 - bytes;
292 ctx->prev_ofs = i;
293 for (; i < 64; i++)
294 ctx->prev[i] = out.b[i];
295 }
296}
297
diff --git a/cbits/cryptonite_salsa.h b/cbits/cryptonite_salsa.h
new file mode 100644
index 00000000..33e9cda9
--- /dev/null
+++ b/cbits/cryptonite_salsa.h
@@ -0,0 +1,57 @@
1/*
2 * Copyright (c) 2014 Vincent Hanquez <vincent@snarc.org>
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the author nor the names of his contributors
15 * may be used to endorse or promote products derived from this software
16 * without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 */
30#ifndef CRYPTONITE_SALSA
31#define CRYPTONITE_SALSA
32
33typedef union {
34 uint64_t q[8];
35 uint32_t d[16];
36 uint8_t b[64];
37} block;
38
39typedef block cryptonite_salsa_state;
40
41typedef struct {
42 cryptonite_salsa_state st;
43 uint8_t prev[64];
44 uint8_t prev_ofs;
45 uint8_t prev_len;
46 uint8_t nb_rounds;
47} cryptonite_salsa_context;
48
49/* for scrypt */
50void cryptonite_salsa_core_xor(int rounds, block *out, block *in);
51
52void cryptonite_salsa_init_core(cryptonite_salsa_state *st, uint32_t keylen, const uint8_t *key, uint32_t ivlen, const uint8_t *iv);
53void cryptonite_salsa_init(cryptonite_salsa_context *ctx, uint8_t nb_rounds, uint32_t keylen, const uint8_t *key, uint32_t ivlen, const uint8_t *iv);
54void cryptonite_salsa_combine(uint8_t *dst, cryptonite_salsa_context *st, const uint8_t *src, uint32_t bytes);
55void cryptonite_salsa_generate(uint8_t *dst, cryptonite_salsa_context *st, uint32_t bytes);
56
57#endif
diff --git a/cbits/cryptonite_xsalsa.c b/cbits/cryptonite_xsalsa.c
new file mode 100644
index 00000000..6718cd7d
--- /dev/null
+++ b/cbits/cryptonite_xsalsa.c
@@ -0,0 +1,80 @@
1/*
2 * Copyright (c) 2016 Brandon Hamilton <brandon.hamilton@gmail.com>
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the author nor the names of his contributors
15 * may be used to endorse or promote products derived from this software
16 * without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 */
30#include <stdint.h>
31#include <string.h>
32#include "cryptonite_xsalsa.h"
33#include "cryptonite_bitfn.h"
34
35static inline uint32_t load32(const uint8_t *p)
36{
37 return le32_to_cpu(*((uint32_t *) p));
38}
39
40/* XSalsa20 algorithm as described in https://cr.yp.to/snuffle/xsalsa-20081128.pdf */
41void cryptonite_xsalsa_init(cryptonite_salsa_context *ctx, uint8_t nb_rounds,
42 uint32_t keylen, const uint8_t *key,
43 uint32_t ivlen, const uint8_t *iv)
44{
45 memset(ctx, 0, sizeof(*ctx));
46 ctx->nb_rounds = nb_rounds;
47
48 /* Create initial 512-bit input block:
49 (x0, x5, x10, x15) is the Salsa20 constant
50 (x1, x2, x3, x4, x11, x12, x13, x14) is a 256-bit key
51 (x6, x7, x8, x9) is the first 128 bits of a 192-bit nonce
52 */
53 cryptonite_salsa_init_core(&ctx->st, keylen, key, 8, iv);
54 ctx->st.d[ 8] = load32(iv + 8);
55 ctx->st.d[ 9] = load32(iv + 12);
56
57 /* Compute (z0, z1, . . . , z15) = doubleround ^(r/2) (x0, x1, . . . , x15) */
58 block hSalsa;
59 memset(&hSalsa, 0, sizeof(block));
60 cryptonite_salsa_core_xor(nb_rounds, &hSalsa, &ctx->st);
61
62 /* Build a new 512-bit input block (x′0, x′1, . . . , x′15):
63 (x′0, x′5, x′10, x′15) is the Salsa20 constant
64 (x′1,x′2,x′3,x′4,x′11,x′12,x′13,x′14) = (z0,z5,z10,z15,z6,z7,z8,z9)
65 (x′6,x′7) is the last 64 bits of the 192-bit nonce
66 (x′8, x′9) is a 64-bit block counter.
67 */
68 ctx->st.d[ 1] = hSalsa.d[ 0] - ctx->st.d[ 0];
69 ctx->st.d[ 2] = hSalsa.d[ 5] - ctx->st.d[ 5];
70 ctx->st.d[ 3] = hSalsa.d[10] - ctx->st.d[10];
71 ctx->st.d[ 4] = hSalsa.d[15] - ctx->st.d[15];
72 ctx->st.d[11] = hSalsa.d[ 6] - ctx->st.d[ 6];
73 ctx->st.d[12] = hSalsa.d[ 7] - ctx->st.d[ 7];
74 ctx->st.d[13] = hSalsa.d[ 8] - ctx->st.d[ 8];
75 ctx->st.d[14] = hSalsa.d[ 9] - ctx->st.d[ 9];
76 ctx->st.d[ 6] = load32(iv + 16);
77 ctx->st.d[ 7] = load32(iv + 20);
78 ctx->st.d[ 8] = 0;
79 ctx->st.d[ 9] = 0;
80} \ No newline at end of file
diff --git a/cbits/cryptonite_xsalsa.h b/cbits/cryptonite_xsalsa.h
new file mode 100644
index 00000000..73233cee
--- /dev/null
+++ b/cbits/cryptonite_xsalsa.h
@@ -0,0 +1,37 @@
1/*
2 * Copyright (c) 2016 Brandon Hamilton <brandon.hamilton@gmail.com>
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the author nor the names of his contributors
15 * may be used to endorse or promote products derived from this software
16 * without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 * SUCH DAMAGE.
29 */
30#ifndef CRYPTONITE_XSALSA
31#define CRYPTONITE_XSALSA
32
33#include "cryptonite_salsa.h"
34
35void cryptonite_xsalsa_init(cryptonite_salsa_context *ctx, uint8_t nb_rounds, uint32_t keylen, const uint8_t *key, uint32_t ivlen, const uint8_t *iv);
36
37#endif
diff --git a/ci b/ci
new file mode 100755
index 00000000..52e7c6b7
--- /dev/null
+++ b/ci
@@ -0,0 +1,7 @@
1#!/bin/sh
2compile=ghci
3defs="-DBENCODE_AESON -DTHREAD_DEBUG"
4hide="-hide-package crypto-random -hide-package crypto-api -hide-package crypto-numbers -hide-package cryptohash -hide-package prettyclass"
5# cbits="cbits/*.c"
6includes="-isrc -icryptonite-backport"
7$compile -Wmissing-signatures -fdefer-typed-holes -freverse-errors $hide $includes -XOverloadedStrings -XRecordWildCards $defs $cbits "$@"
diff --git a/cryptonite-backport/Crypto/Cipher/Salsa.hs b/cryptonite-backport/Crypto/Cipher/Salsa.hs
new file mode 100644
index 00000000..b6b188b1
--- /dev/null
+++ b/cryptonite-backport/Crypto/Cipher/Salsa.hs
@@ -0,0 +1,83 @@
1-- |
2-- Module : Crypto.Cipher.Salsa
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : stable
6-- Portability : good
7--
8{-# LANGUAGE ForeignFunctionInterface #-}
9{-# LANGUAGE GeneralizedNewtypeDeriving #-}
10module Crypto.Cipher.Salsa
11 ( initialize
12 , combine
13 , generate
14 , State(..)
15 ) where
16
17import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, ScrubbedBytes)
18import qualified Crypto.Internal.ByteArray as B
19import Crypto.Internal.Compat
20import Crypto.Internal.Imports
21import Foreign.Ptr
22import Foreign.C.Types
23
24-- | Salsa context
25newtype State = State ScrubbedBytes
26 deriving (NFData)
27
28-- | Initialize a new Salsa context with the number of rounds,
29-- the key and the nonce associated.
30initialize :: (ByteArrayAccess key, ByteArrayAccess nonce)
31 => Int -- ^ number of rounds (8,12,20)
32 -> key -- ^ the key (128 or 256 bits)
33 -> nonce -- ^ the nonce (64 or 96 bits)
34 -> State -- ^ the initial Salsa state
35initialize nbRounds key nonce
36 | not (kLen `elem` [16,32]) = error "Salsa: key length should be 128 or 256 bits"
37 | not (nonceLen `elem` [8,12]) = error "Salsa: nonce length should be 64 or 96 bits"
38 | not (nbRounds `elem` [8,12,20]) = error "Salsa: rounds should be 8, 12 or 20"
39 | otherwise = unsafeDoIO $ do
40 stPtr <- B.alloc 132 $ \stPtr ->
41 B.withByteArray nonce $ \noncePtr ->
42 B.withByteArray key $ \keyPtr ->
43 ccryptonite_salsa_init stPtr (fromIntegral nbRounds) kLen keyPtr nonceLen noncePtr
44 return $ State stPtr
45 where kLen = B.length key
46 nonceLen = B.length nonce
47
48-- | Combine the salsa output and an arbitrary message with a xor,
49-- and return the combined output and the new state.
50combine :: ByteArray ba
51 => State -- ^ the current Salsa state
52 -> ba -- ^ the source to xor with the generator
53 -> (ba, State)
54combine prevSt@(State prevStMem) src
55 | B.null src = (B.empty, prevSt)
56 | otherwise = unsafeDoIO $ do
57 (out, st) <- B.copyRet prevStMem $ \ctx ->
58 B.alloc (B.length src) $ \dstPtr ->
59 B.withByteArray src $ \srcPtr -> do
60 ccryptonite_salsa_combine dstPtr ctx srcPtr (fromIntegral $ B.length src)
61 return (out, State st)
62
63-- | Generate a number of bytes from the Salsa output directly
64generate :: ByteArray ba
65 => State -- ^ the current Salsa state
66 -> Int -- ^ the length of data to generate
67 -> (ba, State)
68generate prevSt@(State prevStMem) len
69 | len <= 0 = (B.empty, prevSt)
70 | otherwise = unsafeDoIO $ do
71 (out, st) <- B.copyRet prevStMem $ \ctx ->
72 B.alloc len $ \dstPtr ->
73 ccryptonite_salsa_generate dstPtr ctx (fromIntegral len)
74 return (out, State st)
75
76foreign import ccall "cryptonite_salsa_init"
77 ccryptonite_salsa_init :: Ptr State -> Int -> Int -> Ptr Word8 -> Int -> Ptr Word8 -> IO ()
78
79foreign import ccall "cryptonite_salsa_combine"
80 ccryptonite_salsa_combine :: Ptr Word8 -> Ptr State -> Ptr Word8 -> CUInt -> IO ()
81
82foreign import ccall "cryptonite_salsa_generate"
83 ccryptonite_salsa_generate :: Ptr Word8 -> Ptr State -> CUInt -> IO ()
diff --git a/cryptonite-backport/Crypto/Cipher/XSalsa.hs b/cryptonite-backport/Crypto/Cipher/XSalsa.hs
new file mode 100644
index 00000000..494760e2
--- /dev/null
+++ b/cryptonite-backport/Crypto/Cipher/XSalsa.hs
@@ -0,0 +1,50 @@
1-- |
2-- Module : Crypto.Cipher.XSalsa
3-- License : BSD-style
4-- Maintainer : Brandon Hamilton <brandon.hamilton@gmail.com>
5-- Stability : stable
6-- Portability : good
7--
8-- Implementation of XSalsa20 algorithm
9-- <https://cr.yp.to/snuffle/xsalsa-20081128.pdf>
10-- Based on the Salsa20 algorithm with 256 bit key extended with 192 bit nonce
11
12{-# LANGUAGE ForeignFunctionInterface #-}
13module Crypto.Cipher.XSalsa
14 ( initialize
15 , combine
16 , generate
17 , State
18 ) where
19
20import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, ScrubbedBytes)
21import qualified Crypto.Internal.ByteArray as B
22import Crypto.Internal.Compat
23import Crypto.Internal.Imports
24import Foreign.Ptr
25import Foreign.Storable
26import Foreign.C.Types
27import Crypto.Cipher.Salsa hiding (initialize)
28
29-- | Initialize a new XSalsa context with the number of rounds,
30-- the key and the nonce associated.
31initialize :: (ByteArrayAccess key, ByteArrayAccess nonce)
32 => Int -- ^ number of rounds (8,12,20)
33 -> key -- ^ the key (256 bits)
34 -> nonce -- ^ the nonce (192 bits)
35 -> State -- ^ the initial XSalsa state
36initialize nbRounds key nonce
37 | kLen /= 32 = error "XSalsa: key length should be 256 bits"
38 | nonceLen /= 24 = error "XSalsa: nonce length should be 192 bits"
39 | not (nbRounds `elem` [8,12,20]) = error "XSalsa: rounds should be 8, 12 or 20"
40 | otherwise = unsafeDoIO $ do
41 stPtr <- B.alloc 132 $ \stPtr ->
42 B.withByteArray nonce $ \noncePtr ->
43 B.withByteArray key $ \keyPtr ->
44 ccryptonite_xsalsa_init stPtr (fromIntegral nbRounds) kLen keyPtr nonceLen noncePtr
45 return $ State stPtr
46 where kLen = B.length key
47 nonceLen = B.length nonce
48
49foreign import ccall "cryptonite_xsalsa_init"
50 ccryptonite_xsalsa_init :: Ptr State -> Int -> Int -> Ptr Word8 -> Int -> Ptr Word8 -> IO ()
diff --git a/cryptonite-backport/Crypto/ECC/Class.hs b/cryptonite-backport/Crypto/ECC/Class.hs
new file mode 100644
index 00000000..16b2cc15
--- /dev/null
+++ b/cryptonite-backport/Crypto/ECC/Class.hs
@@ -0,0 +1,127 @@
1-- |
2-- Module : Crypto.ECC.Class
3-- License : BSD-style
4-- Stability : experimental
5-- Portability : unknown
6--
7-- Elliptic Curve Cryptography
8--
9{-# LANGUAGE GeneralizedNewtypeDeriving #-}
10{-# LANGUAGE TypeFamilies #-}
11{-# LANGUAGE ScopedTypeVariables #-}
12module Crypto.ECC.Class
13 ( Curve_X25519(..)
14 , EllipticCurve(..)
15 , EllipticCurveDH(..)
16 , EllipticCurveArith(..)
17 , KeyPair(..)
18 , SharedSecret(..)
19 ) where
20
21import qualified Crypto.ECC.Simple.Types as Simple
22import qualified Crypto.ECC.Simple.Prim as Simple
23import Crypto.Random
24-- import Crypto.Error
25import Crypto.Error.Types
26-- import Crypto.Internal.Proxy
27import Data.Typeable
28import Crypto.Internal.ByteArray (ByteArray, ByteArrayAccess, ScrubbedBytes)
29import qualified Crypto.Internal.ByteArray as B
30import Crypto.Number.Serialize (i2ospOf_, os2ip)
31import qualified Crypto.PubKey.Curve25519 as X25519
32import Data.ByteArray (convert)
33
34-- | An elliptic curve key pair composed of the private part (a scalar), and
35-- the associated point.
36data KeyPair curve = KeyPair
37 { keypairGetPublic :: !(Point curve)
38 , keypairGetPrivate :: !(Scalar curve)
39 }
40
41newtype SharedSecret = SharedSecret ScrubbedBytes
42 deriving (Eq, ByteArrayAccess)
43
44class EllipticCurve curve where
45 -- | Point on an Elliptic Curve
46 type Point curve :: *
47
48 -- | Scalar in the Elliptic Curve domain
49 type Scalar curve :: *
50
51 -- | Generate a new random scalar on the curve.
52 -- The scalar will represent a number between 1 and the order of the curve non included
53 curveGenerateScalar :: MonadRandom randomly => proxy curve -> randomly (Scalar curve)
54
55 -- | Generate a new random keypair
56 curveGenerateKeyPair :: MonadRandom randomly => proxy curve -> randomly (KeyPair curve)
57
58 -- | Get the curve size in bits
59 curveSizeBits :: proxy curve -> Int
60
61 -- | Encode a elliptic curve point into binary form
62 encodePoint :: ByteArray bs => proxy curve -> Point curve -> bs
63
64 -- | Try to decode the binary form of an elliptic curve point
65 decodePoint :: ByteArray bs => proxy curve -> bs -> CryptoFailable (Point curve)
66
67class EllipticCurve curve => EllipticCurveDH curve where
68 -- | Generate a Diffie hellman secret value.
69 --
70 -- This is generally just the .x coordinate of the resulting point, that
71 -- is not hashed.
72 --
73 -- use `pointSmul` to keep the result in Point format.
74 ecdh :: proxy curve -> Scalar curve -> Point curve -> SharedSecret
75
76class EllipticCurve curve => EllipticCurveArith curve where
77 -- | Add points on a curve
78 pointAdd :: proxy curve -> Point curve -> Point curve -> Point curve
79
80 -- | Scalar Multiplication on a curve
81 pointSmul :: proxy curve -> Scalar curve -> Point curve -> Point curve
82
83-- -- | Scalar Inverse
84-- scalarInverse :: Scalar curve -> Scalar curve
85
86data Curve_X25519 = Curve_X25519
87
88instance EllipticCurve Curve_X25519 where
89 type Point Curve_X25519 = X25519.PublicKey
90 type Scalar Curve_X25519 = X25519.SecretKey
91 curveSizeBits _ = 255
92 curveGenerateScalar _ = X25519.generateSecretKey
93 curveGenerateKeyPair _ = do
94 s <- X25519.generateSecretKey
95 return $ KeyPair (X25519.toPublic s) s
96 encodePoint _ p = B.convert p
97 decodePoint _ bs = X25519.publicKey bs
98
99instance EllipticCurveDH Curve_X25519 where
100 ecdh _ s p = SharedSecret $ convert secret
101 where secret = X25519.dh p s
102
103encodeECPoint :: forall curve bs . (Simple.Curve curve, ByteArray bs) => Simple.Point curve -> bs
104encodeECPoint Simple.PointO = error "encodeECPoint: cannot serialize point at infinity"
105encodeECPoint (Simple.Point x y) = B.concat [uncompressed,xb,yb]
106 where
107 size = Simple.curveSizeBytes (Proxy :: Proxy curve)
108 uncompressed, xb, yb :: bs
109 uncompressed = B.singleton 4
110 xb = i2ospOf_ size x
111 yb = i2ospOf_ size y
112
113decodeECPoint :: (Simple.Curve curve, ByteArray bs) => bs -> CryptoFailable (Simple.Point curve)
114decodeECPoint mxy = case B.uncons mxy of
115 Nothing -> CryptoFailed $ CryptoError_PointSizeInvalid
116 Just (m,xy)
117 -- uncompressed
118 | m == 4 ->
119 let siz = B.length xy `div` 2
120 (xb,yb) = B.splitAt siz xy
121 x = os2ip xb
122 y = os2ip yb
123 in Simple.pointFromIntegers (x,y)
124 | otherwise -> CryptoFailed $ CryptoError_PointFormatInvalid
125
126curveSizeBytes :: EllipticCurve c => Proxy c -> Int
127curveSizeBytes proxy = (curveSizeBits proxy + 7) `div` 8
diff --git a/cryptonite-backport/Crypto/ECC/Simple/Prim.hs b/cryptonite-backport/Crypto/ECC/Simple/Prim.hs
new file mode 100644
index 00000000..117988f2
--- /dev/null
+++ b/cryptonite-backport/Crypto/ECC/Simple/Prim.hs
@@ -0,0 +1,208 @@
1-- | Elliptic Curve Arithmetic.
2--
3-- /WARNING:/ These functions are vulnerable to timing attacks.
4{-# LANGUAGE ScopedTypeVariables #-}
5module Crypto.ECC.Simple.Prim
6 ( scalarGenerate
7 , scalarFromInteger
8 , pointAdd
9 , pointDouble
10 , pointBaseMul
11 , pointMul
12 , pointAddTwoMuls
13 , pointFromIntegers
14 , isPointAtInfinity
15 , isPointValid
16 ) where
17
18import Data.Maybe
19import Data.Typeable
20import Crypto.Internal.Imports
21import Crypto.Number.ModArithmetic
22import Crypto.Number.F2m
23import Crypto.Number.Generate (generateBetween)
24import Crypto.ECC.Simple.Types
25-- import Crypto.Error
26import Crypto.Error.Types
27import Crypto.Random
28
29-- | Generate a valid scalar for a specific Curve
30scalarGenerate :: forall randomly curve . (MonadRandom randomly, Curve curve) => randomly (Scalar curve)
31scalarGenerate =
32 Scalar <$> generateBetween 1 (n - 1)
33 where
34 n = curveEccN $ curveParameters (Proxy :: Proxy curve)
35
36scalarFromInteger :: forall curve . Curve curve => Integer -> CryptoFailable (Scalar curve)
37scalarFromInteger n
38 | n < 0 || n >= mx = CryptoFailed $ CryptoError_EcScalarOutOfBounds
39 | otherwise = CryptoPassed $ Scalar n
40 where
41 mx = case curveType (Proxy :: Proxy curve) of
42 CurveBinary (CurveBinaryParam b) -> b
43 CurvePrime (CurvePrimeParam p) -> p
44
45--TODO: Extract helper function for `fromMaybe PointO...`
46
47-- | Elliptic Curve point negation:
48-- @pointNegate p@ returns point @q@ such that @pointAdd p q == PointO@.
49pointNegate :: Curve curve => Point curve -> Point curve
50pointNegate PointO = PointO
51pointNegate point@(Point x y) =
52 case curveType point of
53 CurvePrime {} -> Point x (-y)
54 CurveBinary {} -> Point x (x `addF2m` y)
55
56-- | Elliptic Curve point addition.
57--
58-- /WARNING:/ Vulnerable to timing attacks.
59pointAdd :: Curve curve => Point curve -> Point curve -> Point curve
60pointAdd PointO PointO = PointO
61pointAdd PointO q = q
62pointAdd p PointO = p
63pointAdd p q
64 | p == q = pointDouble p
65 | p == pointNegate q = PointO
66pointAdd point@(Point xp yp) (Point xq yq) =
67 case ty of
68 CurvePrime (CurvePrimeParam pr) -> fromMaybe PointO $ do
69 s <- divmod (yp - yq) (xp - xq) pr
70 let xr = (s ^ (2::Int) - xp - xq) `mod` pr
71 yr = (s * (xp - xr) - yp) `mod` pr
72 return $ Point xr yr
73 CurveBinary (CurveBinaryParam fx) -> fromMaybe PointO $ do
74 s <- divF2m fx (yp `addF2m` yq) (xp `addF2m` xq)
75 let xr = mulF2m fx s s `addF2m` s `addF2m` xp `addF2m` xq `addF2m` a
76 yr = mulF2m fx s (xp `addF2m` xr) `addF2m` xr `addF2m` yp
77 return $ Point xr yr
78 where
79 ty = curveType point
80 cc = curveParameters point
81 a = curveEccA cc
82
83-- | Elliptic Curve point doubling.
84--
85-- /WARNING:/ Vulnerable to timing attacks.
86--
87-- This perform the following calculation:
88-- > lambda = (3 * xp ^ 2 + a) / 2 yp
89-- > xr = lambda ^ 2 - 2 xp
90-- > yr = lambda (xp - xr) - yp
91--
92-- With binary curve:
93-- > xp == 0 => P = O
94-- > otherwise =>
95-- > s = xp + (yp / xp)
96-- > xr = s ^ 2 + s + a
97-- > yr = xp ^ 2 + (s+1) * xr
98--
99pointDouble :: Curve curve => Point curve -> Point curve
100pointDouble PointO = PointO
101pointDouble point@(Point xp yp) =
102 case ty of
103 CurvePrime (CurvePrimeParam pr) -> fromMaybe PointO $ do
104 lambda <- divmod (3 * xp ^ (2::Int) + a) (2 * yp) pr
105 let xr = (lambda ^ (2::Int) - 2 * xp) `mod` pr
106 yr = (lambda * (xp - xr) - yp) `mod` pr
107 return $ Point xr yr
108 CurveBinary (CurveBinaryParam fx)
109 | xp == 0 -> PointO
110 | otherwise -> fromMaybe PointO $ do
111 s <- return . addF2m xp =<< divF2m fx yp xp
112 let xr = mulF2m fx s s `addF2m` s `addF2m` a
113 yr = mulF2m fx xp xp `addF2m` mulF2m fx xr (s `addF2m` 1)
114 return $ Point xr yr
115 where
116 ty = curveType point
117 cc = curveParameters point
118 a = curveEccA cc
119
120-- | Elliptic curve point multiplication using the base
121--
122-- /WARNING:/ Vulnerable to timing attacks.
123pointBaseMul :: Curve curve => Scalar curve -> Point curve
124pointBaseMul n = pointMul n (curveEccG $ curveParameters (Proxy :: Proxy curve))
125
126-- | Elliptic curve point multiplication (double and add algorithm).
127--
128-- /WARNING:/ Vulnerable to timing attacks.
129pointMul :: Curve curve => Scalar curve -> Point curve -> Point curve
130pointMul _ PointO = PointO
131pointMul (Scalar n) p
132 | n == 0 = PointO
133 | n == 1 = p
134 | odd n = pointAdd p (pointMul (Scalar (n - 1)) p)
135 | otherwise = pointMul (Scalar (n `div` 2)) (pointDouble p)
136
137-- | Elliptic curve double-scalar multiplication (uses Shamir's trick).
138--
139-- > pointAddTwoMuls n1 p1 n2 p2 == pointAdd (pointMul n1 p1)
140-- > (pointMul n2 p2)
141--
142-- /WARNING:/ Vulnerable to timing attacks.
143pointAddTwoMuls :: Curve curve => Scalar curve -> Point curve -> Scalar curve -> Point curve -> Point curve
144pointAddTwoMuls _ PointO _ PointO = PointO
145pointAddTwoMuls _ PointO n2 p2 = pointMul n2 p2
146pointAddTwoMuls n1 p1 _ PointO = pointMul n1 p1
147pointAddTwoMuls (Scalar n1) p1 (Scalar n2) p2 = go (n1, n2)
148 where
149 p0 = pointAdd p1 p2
150
151 go (0, 0 ) = PointO
152 go (k1, k2) =
153 let q = pointDouble $ go (k1 `div` 2, k2 `div` 2)
154 in case (odd k1, odd k2) of
155 (True , True ) -> pointAdd p0 q
156 (True , False ) -> pointAdd p1 q
157 (False , True ) -> pointAdd p2 q
158 (False , False ) -> q
159
160-- | Check if a point is the point at infinity.
161isPointAtInfinity :: Point curve -> Bool
162isPointAtInfinity PointO = True
163isPointAtInfinity _ = False
164
165-- | Make a point on a curve from integer (x,y) coordinate
166--
167-- if the point is not valid related to the curve then an error is
168-- returned instead of a point
169pointFromIntegers :: forall curve . Curve curve => (Integer, Integer) -> CryptoFailable (Point curve)
170pointFromIntegers (x,y)
171 | isPointValid (Proxy :: Proxy curve) x y = CryptoPassed $ Point x y
172 | otherwise = CryptoFailed $ CryptoError_PointCoordinatesInvalid
173
174-- | check if a point is on specific curve
175--
176-- This perform three checks:
177--
178-- * x is not out of range
179-- * y is not out of range
180-- * the equation @y^2 = x^3 + a*x + b (mod p)@ holds
181isPointValid :: Curve curve => proxy curve -> Integer -> Integer -> Bool
182isPointValid proxy x y =
183 case ty of
184 CurvePrime (CurvePrimeParam p) ->
185 let a = curveEccA cc
186 b = curveEccB cc
187 eqModP z1 z2 = (z1 `mod` p) == (z2 `mod` p)
188 isValid e = e >= 0 && e < p
189 in isValid x && isValid y && (y ^ (2 :: Int)) `eqModP` (x ^ (3 :: Int) + a * x + b)
190 CurveBinary (CurveBinaryParam fx) ->
191 let a = curveEccA cc
192 b = curveEccB cc
193 add = addF2m
194 mul = mulF2m fx
195 isValid e = modF2m fx e == e
196 in and [ isValid x
197 , isValid y
198 , ((((x `add` a) `mul` x `add` y) `mul` x) `add` b `add` (squareF2m fx y)) == 0
199 ]
200 where
201 ty = curveType proxy
202 cc = curveParameters proxy
203
204-- | div and mod
205divmod :: Integer -> Integer -> Integer -> Maybe Integer
206divmod y x m = do
207 i <- inverse (x `mod` m) m
208 return $ y * i `mod` m
diff --git a/cryptonite-backport/Crypto/ECC/Simple/Types.hs b/cryptonite-backport/Crypto/ECC/Simple/Types.hs
new file mode 100644
index 00000000..c97daa29
--- /dev/null
+++ b/cryptonite-backport/Crypto/ECC/Simple/Types.hs
@@ -0,0 +1,615 @@
1{-# LANGUAGE DeriveDataTypeable #-}
2-- |
3-- Module : Crypto.ECC.Simple.Types
4-- License : BSD-style
5-- Maintainer : Vincent Hanquez <vincent@snarc.org>
6-- Stability : Experimental
7-- Portability : Excellent
8--
9-- references:
10-- <https://tools.ietf.org/html/rfc5915>
11--
12{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
13module Crypto.ECC.Simple.Types
14 ( Curve(..)
15 , Point(..)
16 , Scalar(..)
17 , CurveType(..)
18 , CurveBinaryParam(..)
19 , CurvePrimeParam(..)
20 , curveSizeBits
21 , curveSizeBytes
22 , CurveParameters(..)
23 -- * specific curves definition
24 , SEC_p112r1(..)
25 , SEC_p112r2(..)
26 , SEC_p128r1(..)
27 , SEC_p128r2(..)
28 , SEC_p160k1(..)
29 , SEC_p160r1(..)
30 , SEC_p160r2(..)
31 , SEC_p192k1(..)
32 , SEC_p192r1(..) -- aka prime192v1
33 , SEC_p224k1(..)
34 , SEC_p224r1(..)
35 , SEC_p256k1(..)
36 , SEC_p256r1(..) -- aka prime256v1
37 , SEC_p384r1(..)
38 , SEC_p521r1(..)
39 , SEC_t113r1(..)
40 , SEC_t113r2(..)
41 , SEC_t131r1(..)
42 , SEC_t131r2(..)
43 , SEC_t163k1(..)
44 , SEC_t163r1(..)
45 , SEC_t163r2(..)
46 , SEC_t193r1(..)
47 , SEC_t193r2(..)
48 , SEC_t233k1(..) -- aka NIST K-233
49 , SEC_t233r1(..)
50 , SEC_t239k1(..)
51 , SEC_t283k1(..)
52 , SEC_t283r1(..)
53 , SEC_t409k1(..)
54 , SEC_t409r1(..)
55 , SEC_t571k1(..)
56 , SEC_t571r1(..)
57 ) where
58
59import Data.Data
60import Crypto.Internal.Imports
61import Crypto.Number.Basic (numBits)
62
63class Curve curve where
64 curveParameters :: proxy curve -> CurveParameters curve
65 curveType :: proxy curve -> CurveType
66
67-- | get the size of the curve in bits
68curveSizeBits :: Curve curve => proxy curve -> Int
69curveSizeBits proxy =
70 case curveType proxy of
71 CurvePrime (CurvePrimeParam p) -> numBits p
72 CurveBinary (CurveBinaryParam c) -> numBits c - 1
73
74-- | get the size of the curve in bytes
75curveSizeBytes :: Curve curve => proxy curve -> Int
76curveSizeBytes proxy = (curveSizeBits proxy + 7) `div` 8
77
78-- | Define common parameters in a curve definition
79-- of the form: y^2 = x^3 + ax + b.
80data CurveParameters curve = CurveParameters
81 { curveEccA :: Integer -- ^ curve parameter a
82 , curveEccB :: Integer -- ^ curve parameter b
83 , curveEccG :: Point curve -- ^ base point
84 , curveEccN :: Integer -- ^ order of G
85 , curveEccH :: Integer -- ^ cofactor
86 } deriving (Show,Eq,Data,Typeable)
87
88newtype CurveBinaryParam = CurveBinaryParam Integer
89 deriving (Show,Read,Eq,Data,Typeable)
90
91newtype CurvePrimeParam = CurvePrimeParam Integer
92 deriving (Show,Read,Eq,Data,Typeable)
93
94data CurveType =
95 CurveBinary CurveBinaryParam
96 | CurvePrime CurvePrimeParam
97 deriving (Show,Read,Eq,Data,Typeable)
98
99-- | ECC Private Number
100newtype Scalar curve = Scalar Integer
101 deriving (Show,Read,Eq,Data,Typeable)
102
103-- | Define a point on a curve.
104data Point curve =
105 Point Integer Integer
106 | PointO -- ^ Point at Infinity
107 deriving (Show,Read,Eq,Data,Typeable)
108
109instance NFData (Point curve) where
110 rnf (Point x y) = x `seq` y `seq` ()
111 rnf PointO = ()
112
113data SEC_p112r1 = SEC_p112r1 deriving (Show,Read,Eq)
114data SEC_p112r2 = SEC_p112r2 deriving (Show,Read,Eq)
115data SEC_p128r1 = SEC_p128r1 deriving (Show,Read,Eq)
116data SEC_p128r2 = SEC_p128r2 deriving (Show,Read,Eq)
117data SEC_p160k1 = SEC_p160k1 deriving (Show,Read,Eq)
118data SEC_p160r1 = SEC_p160r1 deriving (Show,Read,Eq)
119data SEC_p160r2 = SEC_p160r2 deriving (Show,Read,Eq)
120data SEC_p192k1 = SEC_p192k1 deriving (Show,Read,Eq)
121data SEC_p192r1 = SEC_p192r1 deriving (Show,Read,Eq)
122data SEC_p224k1 = SEC_p224k1 deriving (Show,Read,Eq)
123data SEC_p224r1 = SEC_p224r1 deriving (Show,Read,Eq)
124data SEC_p256k1 = SEC_p256k1 deriving (Show,Read,Eq)
125data SEC_p256r1 = SEC_p256r1 deriving (Show,Read,Eq)
126data SEC_p384r1 = SEC_p384r1 deriving (Show,Read,Eq)
127data SEC_p521r1 = SEC_p521r1 deriving (Show,Read,Eq)
128data SEC_t113r1 = SEC_t113r1 deriving (Show,Read,Eq)
129data SEC_t113r2 = SEC_t113r2 deriving (Show,Read,Eq)
130data SEC_t131r1 = SEC_t131r1 deriving (Show,Read,Eq)
131data SEC_t131r2 = SEC_t131r2 deriving (Show,Read,Eq)
132data SEC_t163k1 = SEC_t163k1 deriving (Show,Read,Eq)
133data SEC_t163r1 = SEC_t163r1 deriving (Show,Read,Eq)
134data SEC_t163r2 = SEC_t163r2 deriving (Show,Read,Eq)
135data SEC_t193r1 = SEC_t193r1 deriving (Show,Read,Eq)
136data SEC_t193r2 = SEC_t193r2 deriving (Show,Read,Eq)
137data SEC_t233k1 = SEC_t233k1 deriving (Show,Read,Eq)
138data SEC_t233r1 = SEC_t233r1 deriving (Show,Read,Eq)
139data SEC_t239k1 = SEC_t239k1 deriving (Show,Read,Eq)
140data SEC_t283k1 = SEC_t283k1 deriving (Show,Read,Eq)
141data SEC_t283r1 = SEC_t283r1 deriving (Show,Read,Eq)
142data SEC_t409k1 = SEC_t409k1 deriving (Show,Read,Eq)
143data SEC_t409r1 = SEC_t409r1 deriving (Show,Read,Eq)
144data SEC_t571k1 = SEC_t571k1 deriving (Show,Read,Eq)
145data SEC_t571r1 = SEC_t571r1 deriving (Show,Read,Eq)
146
147-- | Define names for known recommended curves.
148instance Curve SEC_p112r1 where
149 curveType _ = typeSEC_p112r1
150 curveParameters _ = paramSEC_p112r1
151
152instance Curve SEC_p112r2 where
153 curveType _ = typeSEC_p112r2
154 curveParameters _ = paramSEC_p112r2
155
156instance Curve SEC_p128r1 where
157 curveType _ = typeSEC_p128r1
158 curveParameters _ = paramSEC_p128r1
159
160instance Curve SEC_p128r2 where
161 curveType _ = typeSEC_p128r2
162 curveParameters _ = paramSEC_p128r2
163
164instance Curve SEC_p160k1 where
165 curveType _ = typeSEC_p160k1
166 curveParameters _ = paramSEC_p160k1
167
168instance Curve SEC_p160r1 where
169 curveType _ = typeSEC_p160r1
170 curveParameters _ = paramSEC_p160r1
171
172instance Curve SEC_p160r2 where
173 curveType _ = typeSEC_p160r2
174 curveParameters _ = paramSEC_p160r2
175
176instance Curve SEC_p192k1 where
177 curveType _ = typeSEC_p192k1
178 curveParameters _ = paramSEC_p192k1
179
180instance Curve SEC_p192r1 where
181 curveType _ = typeSEC_p192r1
182 curveParameters _ = paramSEC_p192r1
183
184instance Curve SEC_p224k1 where
185 curveType _ = typeSEC_p224k1
186 curveParameters _ = paramSEC_p224k1
187
188instance Curve SEC_p224r1 where
189 curveType _ = typeSEC_p224r1
190 curveParameters _ = paramSEC_p224r1
191
192instance Curve SEC_p256k1 where
193 curveType _ = typeSEC_p256k1
194 curveParameters _ = paramSEC_p256k1
195
196instance Curve SEC_p256r1 where
197 curveType _ = typeSEC_p256r1
198 curveParameters _ = paramSEC_p256r1
199
200instance Curve SEC_p384r1 where
201 curveType _ = typeSEC_p384r1
202 curveParameters _ = paramSEC_p384r1
203
204instance Curve SEC_p521r1 where
205 curveType _ = typeSEC_p521r1
206 curveParameters _ = paramSEC_p521r1
207
208instance Curve SEC_t113r1 where
209 curveType _ = typeSEC_t113r1
210 curveParameters _ = paramSEC_t113r1
211
212instance Curve SEC_t113r2 where
213 curveType _ = typeSEC_t113r2
214 curveParameters _ = paramSEC_t113r2
215
216instance Curve SEC_t131r1 where
217 curveType _ = typeSEC_t131r1
218 curveParameters _ = paramSEC_t131r1
219
220instance Curve SEC_t131r2 where
221 curveType _ = typeSEC_t131r2
222 curveParameters _ = paramSEC_t131r2
223
224instance Curve SEC_t163k1 where
225 curveType _ = typeSEC_t163k1
226 curveParameters _ = paramSEC_t163k1
227
228instance Curve SEC_t163r1 where
229 curveType _ = typeSEC_t163r1
230 curveParameters _ = paramSEC_t163r1
231
232instance Curve SEC_t163r2 where
233 curveType _ = typeSEC_t163r2
234 curveParameters _ = paramSEC_t163r2
235
236instance Curve SEC_t193r1 where
237 curveType _ = typeSEC_t193r1
238 curveParameters _ = paramSEC_t193r1
239
240instance Curve SEC_t193r2 where
241 curveType _ = typeSEC_t193r2
242 curveParameters _ = paramSEC_t193r2
243
244instance Curve SEC_t233k1 where
245 curveType _ = typeSEC_t233k1
246 curveParameters _ = paramSEC_t233k1
247
248instance Curve SEC_t233r1 where
249 curveType _ = typeSEC_t233r1
250 curveParameters _ = paramSEC_t233r1
251
252instance Curve SEC_t239k1 where
253 curveType _ = typeSEC_t239k1
254 curveParameters _ = paramSEC_t239k1
255
256instance Curve SEC_t283k1 where
257 curveType _ = typeSEC_t283k1
258 curveParameters _ = paramSEC_t283k1
259
260instance Curve SEC_t283r1 where
261 curveType _ = typeSEC_t283r1
262 curveParameters _ = paramSEC_t283r1
263
264instance Curve SEC_t409k1 where
265 curveType _ = typeSEC_t409k1
266 curveParameters _ = paramSEC_t409k1
267
268instance Curve SEC_t409r1 where
269 curveType _ = typeSEC_t409r1
270 curveParameters _ = paramSEC_t409r1
271
272instance Curve SEC_t571k1 where
273 curveType _ = typeSEC_t571k1
274 curveParameters _ = paramSEC_t571k1
275
276instance Curve SEC_t571r1 where
277 curveType _ = typeSEC_t571r1
278 curveParameters _ = paramSEC_t571r1
279
280{-
281curvesOIDs :: [ (CurveName, [Integer]) ]
282curvesOIDs =
283 [ (SEC_p112r1, [1,3,132,0,6])
284 , (SEC_p112r2, [1,3,132,0,7])
285 , (SEC_p128r1, [1,3,132,0,28])
286 , (SEC_p128r2, [1,3,132,0,29])
287 , (SEC_p160k1, [1,3,132,0,9])
288 , (SEC_p160r1, [1,3,132,0,8])
289 , (SEC_p160r2, [1,3,132,0,30])
290 , (SEC_p192k1, [1,3,132,0,31])
291 , (SEC_p192r1, [1,2,840,10045,3,1,1])
292 , (SEC_p224k1, [1,3,132,0,32])
293 , (SEC_p224r1, [1,3,132,0,33])
294 , (SEC_p256k1, [1,3,132,0,10])
295 , (SEC_p256r1, [1,2,840,10045,3,1,7])
296 , (SEC_p384r1, [1,3,132,0,34])
297 , (SEC_p521r1, [1,3,132,0,35])
298 , (SEC_t113r1, [1,3,132,0,4])
299 , (SEC_t113r2, [1,3,132,0,5])
300 , (SEC_t131r1, [1,3,132,0,22])
301 , (SEC_t131r2, [1,3,132,0,23])
302 , (SEC_t163k1, [1,3,132,0,1])
303 , (SEC_t163r1, [1,3,132,0,2])
304 , (SEC_t163r2, [1,3,132,0,15])
305 , (SEC_t193r1, [1,3,132,0,24])
306 , (SEC_t193r2, [1,3,132,0,25])
307 , (SEC_t233k1, [1,3,132,0,26])
308 , (SEC_t233r1, [1,3,132,0,27])
309 , (SEC_t239k1, [1,3,132,0,3])
310 , (SEC_t283k1, [1,3,132,0,16])
311 , (SEC_t283r1, [1,3,132,0,17])
312 , (SEC_t409k1, [1,3,132,0,36])
313 , (SEC_t409r1, [1,3,132,0,37])
314 , (SEC_t571k1, [1,3,132,0,38])
315 , (SEC_t571r1, [1,3,132,0,39])
316 ]
317-}
318
319typeSEC_p112r1 = CurvePrime $ CurvePrimeParam 0xdb7c2abf62e35e668076bead208b
320paramSEC_p112r1 = CurveParameters
321 { curveEccA = 0xdb7c2abf62e35e668076bead2088
322 , curveEccB = 0x659ef8ba043916eede8911702b22
323 , curveEccG = Point 0x09487239995a5ee76b55f9c2f098
324 0xa89ce5af8724c0a23e0e0ff77500
325 , curveEccN = 0xdb7c2abf62e35e7628dfac6561c5
326 , curveEccH = 1
327 }
328typeSEC_p112r2 = CurvePrime $ CurvePrimeParam 0xdb7c2abf62e35e668076bead208b
329paramSEC_p112r2 = CurveParameters
330 { curveEccA = 0x6127c24c05f38a0aaaf65c0ef02c
331 , curveEccB = 0x51def1815db5ed74fcc34c85d709
332 , curveEccG = Point 0x4ba30ab5e892b4e1649dd0928643
333 0xadcd46f5882e3747def36e956e97
334 , curveEccN = 0x36df0aafd8b8d7597ca10520d04b
335 , curveEccH = 4
336 }
337typeSEC_p128r1 = CurvePrime $ CurvePrimeParam 0xfffffffdffffffffffffffffffffffff
338paramSEC_p128r1 = CurveParameters
339 { curveEccA = 0xfffffffdfffffffffffffffffffffffc
340 , curveEccB = 0xe87579c11079f43dd824993c2cee5ed3
341 , curveEccG = Point 0x161ff7528b899b2d0c28607ca52c5b86
342 0xcf5ac8395bafeb13c02da292dded7a83
343 , curveEccN = 0xfffffffe0000000075a30d1b9038a115
344 , curveEccH = 1
345 }
346typeSEC_p128r2 = CurvePrime $ CurvePrimeParam 0xfffffffdffffffffffffffffffffffff
347paramSEC_p128r2 = CurveParameters
348 { curveEccA = 0xd6031998d1b3bbfebf59cc9bbff9aee1
349 , curveEccB = 0x5eeefca380d02919dc2c6558bb6d8a5d
350 , curveEccG = Point 0x7b6aa5d85e572983e6fb32a7cdebc140
351 0x27b6916a894d3aee7106fe805fc34b44
352 , curveEccN = 0x3fffffff7fffffffbe0024720613b5a3
353 , curveEccH = 4
354 }
355typeSEC_p160k1 = CurvePrime $ CurvePrimeParam 0x00fffffffffffffffffffffffffffffffeffffac73
356paramSEC_p160k1 = CurveParameters
357 { curveEccA = 0x000000000000000000000000000000000000000000
358 , curveEccB = 0x000000000000000000000000000000000000000007
359 , curveEccG = Point 0x003b4c382ce37aa192a4019e763036f4f5dd4d7ebb
360 0x00938cf935318fdced6bc28286531733c3f03c4fee
361 , curveEccN = 0x0100000000000000000001b8fa16dfab9aca16b6b3
362 , curveEccH = 1
363 }
364typeSEC_p160r1 = CurvePrime $ CurvePrimeParam 0x00ffffffffffffffffffffffffffffffff7fffffff
365paramSEC_p160r1 = CurveParameters
366 { curveEccA = 0x00ffffffffffffffffffffffffffffffff7ffffffc
367 , curveEccB = 0x001c97befc54bd7a8b65acf89f81d4d4adc565fa45
368 , curveEccG = Point 0x004a96b5688ef573284664698968c38bb913cbfc82
369 0x0023a628553168947d59dcc912042351377ac5fb32
370 , curveEccN = 0x0100000000000000000001f4c8f927aed3ca752257
371 , curveEccH = 1
372 }
373typeSEC_p160r2 = CurvePrime $ CurvePrimeParam 0x00fffffffffffffffffffffffffffffffeffffac73
374paramSEC_p160r2 = CurveParameters
375 { curveEccA = 0x00fffffffffffffffffffffffffffffffeffffac70
376 , curveEccB = 0x00b4e134d3fb59eb8bab57274904664d5af50388ba
377 , curveEccG = Point 0x0052dcb034293a117e1f4ff11b30f7199d3144ce6d
378 0x00feaffef2e331f296e071fa0df9982cfea7d43f2e
379 , curveEccN = 0x0100000000000000000000351ee786a818f3a1a16b
380 , curveEccH = 1
381 }
382typeSEC_p192k1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffffffffffeffffee37
383paramSEC_p192k1 = CurveParameters
384 { curveEccA = 0x000000000000000000000000000000000000000000000000
385 , curveEccB = 0x000000000000000000000000000000000000000000000003
386 , curveEccG = Point 0xdb4ff10ec057e9ae26b07d0280b7f4341da5d1b1eae06c7d
387 0x9b2f2f6d9c5628a7844163d015be86344082aa88d95e2f9d
388 , curveEccN = 0xfffffffffffffffffffffffe26f2fc170f69466a74defd8d
389 , curveEccH = 1
390 }
391typeSEC_p192r1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffeffffffffffffffff
392paramSEC_p192r1 = CurveParameters
393 { curveEccA = 0xfffffffffffffffffffffffffffffffefffffffffffffffc
394 , curveEccB = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
395 , curveEccG = Point 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012
396 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811
397 , curveEccN = 0xffffffffffffffffffffffff99def836146bc9b1b4d22831
398 , curveEccH = 1
399 }
400typeSEC_p224k1 = CurvePrime $ CurvePrimeParam 0x00fffffffffffffffffffffffffffffffffffffffffffffffeffffe56d
401paramSEC_p224k1 = CurveParameters
402 { curveEccA = 0x0000000000000000000000000000000000000000000000000000000000
403 , curveEccB = 0x0000000000000000000000000000000000000000000000000000000005
404 , curveEccG = Point 0x00a1455b334df099df30fc28a169a467e9e47075a90f7e650eb6b7a45c
405 0x007e089fed7fba344282cafbd6f7e319f7c0b0bd59e2ca4bdb556d61a5
406 , curveEccN = 0x010000000000000000000000000001dce8d2ec6184caf0a971769fb1f7
407 , curveEccH = 1
408 }
409typeSEC_p224r1 = CurvePrime $ CurvePrimeParam 0xffffffffffffffffffffffffffffffff000000000000000000000001
410paramSEC_p224r1 = CurveParameters
411 { curveEccA = 0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe
412 , curveEccB = 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4
413 , curveEccG = Point 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21
414 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34
415 , curveEccN = 0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d
416 , curveEccH = 1
417 }
418typeSEC_p256k1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
419paramSEC_p256k1 = CurveParameters
420 { curveEccA = 0x0000000000000000000000000000000000000000000000000000000000000000
421 , curveEccB = 0x0000000000000000000000000000000000000000000000000000000000000007
422 , curveEccG = Point 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
423 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
424 , curveEccN = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
425 , curveEccH = 1
426 }
427typeSEC_p256r1 = CurvePrime $ CurvePrimeParam 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
428paramSEC_p256r1 = CurveParameters
429 { curveEccA = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
430 , curveEccB = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
431 , curveEccG = Point 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296
432 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5
433 , curveEccN = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
434 , curveEccH = 1
435 }
436typeSEC_p384r1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff
437paramSEC_p384r1 = CurveParameters
438 { curveEccA = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc
439 , curveEccB = 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef
440 , curveEccG = Point 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7
441 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f
442 , curveEccN = 0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973
443 , curveEccH = 1
444 }
445typeSEC_p521r1 = CurvePrime $ CurvePrimeParam 0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
446paramSEC_p521r1 = CurveParameters
447 { curveEccA = 0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc
448 , curveEccB = 0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00
449 , curveEccG = Point 0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66
450 0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650
451 , curveEccN = 0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409
452 , curveEccH = 1
453 }
454typeSEC_t113r1 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000201
455paramSEC_t113r1 = CurveParameters
456 { curveEccA = 0x003088250ca6e7c7fe649ce85820f7
457 , curveEccB = 0x00e8bee4d3e2260744188be0e9c723
458 , curveEccG = Point 0x009d73616f35f4ab1407d73562c10f
459 0x00a52830277958ee84d1315ed31886
460 , curveEccN = 0x0100000000000000d9ccec8a39e56f
461 , curveEccH = 2
462 }
463typeSEC_t113r2 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000201
464paramSEC_t113r2 = CurveParameters
465 { curveEccA = 0x00689918dbec7e5a0dd6dfc0aa55c7
466 , curveEccB = 0x0095e9a9ec9b297bd4bf36e059184f
467 , curveEccG = Point 0x01a57a6a7b26ca5ef52fcdb8164797
468 0x00b3adc94ed1fe674c06e695baba1d
469 , curveEccN = 0x010000000000000108789b2496af93
470 , curveEccH = 2
471 }
472typeSEC_t131r1 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000010d
473paramSEC_t131r1 = CurveParameters
474 { curveEccA = 0x07a11b09a76b562144418ff3ff8c2570b8
475 , curveEccB = 0x0217c05610884b63b9c6c7291678f9d341
476 , curveEccG = Point 0x0081baf91fdf9833c40f9c181343638399
477 0x078c6e7ea38c001f73c8134b1b4ef9e150
478 , curveEccN = 0x0400000000000000023123953a9464b54d
479 , curveEccH = 2
480 }
481typeSEC_t131r2 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000010d
482paramSEC_t131r2 = CurveParameters
483 { curveEccA = 0x03e5a88919d7cafcbf415f07c2176573b2
484 , curveEccB = 0x04b8266a46c55657ac734ce38f018f2192
485 , curveEccG = Point 0x0356dcd8f2f95031ad652d23951bb366a8
486 0x0648f06d867940a5366d9e265de9eb240f
487 , curveEccN = 0x0400000000000000016954a233049ba98f
488 , curveEccH = 2
489 }
490typeSEC_t163k1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000c9
491paramSEC_t163k1 = CurveParameters
492 { curveEccA = 0x000000000000000000000000000000000000000001
493 , curveEccB = 0x000000000000000000000000000000000000000001
494 , curveEccG = Point 0x02fe13c0537bbc11acaa07d793de4e6d5e5c94eee8
495 0x0289070fb05d38ff58321f2e800536d538ccdaa3d9
496 , curveEccN = 0x04000000000000000000020108a2e0cc0d99f8a5ef
497 , curveEccH = 2
498 }
499typeSEC_t163r1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000c9
500paramSEC_t163r1 = CurveParameters
501 { curveEccA = 0x07b6882caaefa84f9554ff8428bd88e246d2782ae2
502 , curveEccB = 0x0713612dcddcb40aab946bda29ca91f73af958afd9
503 , curveEccG = Point 0x0369979697ab43897789566789567f787a7876a654
504 0x00435edb42efafb2989d51fefce3c80988f41ff883
505 , curveEccN = 0x03ffffffffffffffffffff48aab689c29ca710279b
506 , curveEccH = 2
507 }
508typeSEC_t163r2 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000c9
509paramSEC_t163r2 = CurveParameters
510 { curveEccA = 0x000000000000000000000000000000000000000001
511 , curveEccB = 0x020a601907b8c953ca1481eb10512f78744a3205fd
512 , curveEccG = Point 0x03f0eba16286a2d57ea0991168d4994637e8343e36
513 0x00d51fbc6c71a0094fa2cdd545b11c5c0c797324f1
514 , curveEccN = 0x040000000000000000000292fe77e70c12a4234c33
515 , curveEccH = 2
516 }
517typeSEC_t193r1 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000008001
518paramSEC_t193r1 = CurveParameters
519 { curveEccA = 0x0017858feb7a98975169e171f77b4087de098ac8a911df7b01
520 , curveEccB = 0x00fdfb49bfe6c3a89facadaa7a1e5bbc7cc1c2e5d831478814
521 , curveEccG = Point 0x01f481bc5f0ff84a74ad6cdf6fdef4bf6179625372d8c0c5e1
522 0x0025e399f2903712ccf3ea9e3a1ad17fb0b3201b6af7ce1b05
523 , curveEccN = 0x01000000000000000000000000c7f34a778f443acc920eba49
524 , curveEccH = 2
525 }
526typeSEC_t193r2 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000008001
527paramSEC_t193r2 = CurveParameters
528 { curveEccA = 0x0163f35a5137c2ce3ea6ed8667190b0bc43ecd69977702709b
529 , curveEccB = 0x00c9bb9e8927d4d64c377e2ab2856a5b16e3efb7f61d4316ae
530 , curveEccG = Point 0x00d9b67d192e0367c803f39e1a7e82ca14a651350aae617e8f
531 0x01ce94335607c304ac29e7defbd9ca01f596f927224cdecf6c
532 , curveEccN = 0x010000000000000000000000015aab561b005413ccd4ee99d5
533 , curveEccH = 2
534 }
535typeSEC_t233k1 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000000000000000004000000000000000001
536paramSEC_t233k1 = CurveParameters
537 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000
538 , curveEccB = 0x000000000000000000000000000000000000000000000000000000000001
539 , curveEccG = Point 0x017232ba853a7e731af129f22ff4149563a419c26bf50a4c9d6eefad6126
540 0x01db537dece819b7f70f555a67c427a8cd9bf18aeb9b56e0c11056fae6a3
541 , curveEccN = 0x008000000000000000000000000000069d5bb915bcd46efb1ad5f173abdf
542 , curveEccH = 4
543 }
544typeSEC_t233r1 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000000000000000004000000000000000001
545paramSEC_t233r1 = CurveParameters
546 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000001
547 , curveEccB = 0x0066647ede6c332c7f8c0923bb58213b333b20e9ce4281fe115f7d8f90ad
548 , curveEccG = Point 0x00fac9dfcbac8313bb2139f1bb755fef65bc391f8b36f8f8eb7371fd558b
549 0x01006a08a41903350678e58528bebf8a0beff867a7ca36716f7e01f81052
550 , curveEccN = 0x01000000000000000000000000000013e974e72f8a6922031d2603cfe0d7
551 , curveEccH = 2
552 }
553typeSEC_t239k1 = CurveBinary $ CurveBinaryParam 0x800000000000000000004000000000000000000000000000000000000001
554paramSEC_t239k1 = CurveParameters
555 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000
556 , curveEccB = 0x000000000000000000000000000000000000000000000000000000000001
557 , curveEccG = Point 0x29a0b6a887a983e9730988a68727a8b2d126c44cc2cc7b2a6555193035dc
558 0x76310804f12e549bdb011c103089e73510acb275fc312a5dc6b76553f0ca
559 , curveEccN = 0x2000000000000000000000000000005a79fec67cb6e91f1c1da800e478a5
560 , curveEccH = 4
561 }
562typeSEC_t283k1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000000000000000000000000000000010a1
563paramSEC_t283k1 = CurveParameters
564 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000000
565 , curveEccB = 0x000000000000000000000000000000000000000000000000000000000000000000000001
566 , curveEccG = Point 0x0503213f78ca44883f1a3b8162f188e553cd265f23c1567a16876913b0c2ac2458492836
567 0x01ccda380f1c9e318d90f95d07e5426fe87e45c0e8184698e45962364e34116177dd2259
568 , curveEccN = 0x01ffffffffffffffffffffffffffffffffffe9ae2ed07577265dff7f94451e061e163c61
569 , curveEccH = 4
570 }
571typeSEC_t283r1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000000000000000000000000000000010a1
572paramSEC_t283r1 = CurveParameters
573 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000001
574 , curveEccB = 0x027b680ac8b8596da5a4af8a19a0303fca97fd7645309fa2a581485af6263e313b79a2f5
575 , curveEccG = Point 0x05f939258db7dd90e1934f8c70b0dfec2eed25b8557eac9c80e2e198f8cdbecd86b12053
576 0x03676854fe24141cb98fe6d4b20d02b4516ff702350eddb0826779c813f0df45be8112f4
577 , curveEccN = 0x03ffffffffffffffffffffffffffffffffffef90399660fc938a90165b042a7cefadb307
578 , curveEccH = 2
579 }
580typeSEC_t409k1 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000001
581paramSEC_t409k1 = CurveParameters
582 { curveEccA = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
583 , curveEccB = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
584 , curveEccG = Point 0x0060f05f658f49c1ad3ab1890f7184210efd0987e307c84c27accfb8f9f67cc2c460189eb5aaaa62ee222eb1b35540cfe9023746
585 0x01e369050b7c4e42acba1dacbf04299c3460782f918ea427e6325165e9ea10e3da5f6c42e9c55215aa9ca27a5863ec48d8e0286b
586 , curveEccN = 0x007ffffffffffffffffffffffffffffffffffffffffffffffffffe5f83b2d4ea20400ec4557d5ed3e3e7ca5b4b5c83b8e01e5fcf
587 , curveEccH = 4
588 }
589typeSEC_t409r1 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000001
590paramSEC_t409r1 = CurveParameters
591 { curveEccA = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
592 , curveEccB = 0x0021a5c2c8ee9feb5c4b9a753b7b476b7fd6422ef1f3dd674761fa99d6ac27c8a9a197b272822f6cd57a55aa4f50ae317b13545f
593 , curveEccG = Point 0x015d4860d088ddb3496b0c6064756260441cde4af1771d4db01ffe5b34e59703dc255a868a1180515603aeab60794e54bb7996a7
594 0x0061b1cfab6be5f32bbfa78324ed106a7636b9c5a7bd198d0158aa4f5488d08f38514f1fdf4b4f40d2181b3681c364ba0273c706
595 , curveEccN = 0x010000000000000000000000000000000000000000000000000001e2aad6a612f33307be5fa47c3c9e052f838164cd37d9a21173
596 , curveEccH = 2
597 }
598typeSEC_t571k1 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000425
599paramSEC_t571k1 = CurveParameters
600 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
601 , curveEccB = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
602 , curveEccG = Point 0x026eb7a859923fbc82189631f8103fe4ac9ca2970012d5d46024804801841ca44370958493b205e647da304db4ceb08cbbd1ba39494776fb988b47174dca88c7e2945283a01c8972
603 0x0349dc807f4fbf374f4aeade3bca95314dd58cec9f307a54ffc61efc006d8a2c9d4979c0ac44aea74fbebbb9f772aedcb620b01a7ba7af1b320430c8591984f601cd4c143ef1c7a3
604 , curveEccN = 0x020000000000000000000000000000000000000000000000000000000000000000000000131850e1f19a63e4b391a8db917f4138b630d84be5d639381e91deb45cfe778f637c1001
605 , curveEccH = 4
606 }
607typeSEC_t571r1 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000425
608paramSEC_t571r1 = CurveParameters
609 { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
610 , curveEccB = 0x02f40e7e2221f295de297117b7f3d62f5c6a97ffcb8ceff1cd6ba8ce4a9a18ad84ffabbd8efa59332be7ad6756a66e294afd185a78ff12aa520e4de739baca0c7ffeff7f2955727a
611 , curveEccG = Point 0x0303001d34b856296c16c0d40d3cd7750a93d1d2955fa80aa5f40fc8db7b2abdbde53950f4c0d293cdd711a35b67fb1499ae60038614f1394abfa3b4c850d927e1e7769c8eec2d19
612 0x037bf27342da639b6dccfffeb73d69d78c6c27a6009cbbca1980f8533921e8a684423e43bab08a576291af8f461bb2a8b3531d2f0485c19b16e2f1516e23dd3c1a4827af1b8ac15b
613 , curveEccN = 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe661ce18ff55987308059b186823851ec7dd9ca1161de93d5174d66e8382e9bb2fe84e47
614 , curveEccH = 2
615 }
diff --git a/cryptonite-backport/Crypto/Error/Types.hs b/cryptonite-backport/Crypto/Error/Types.hs
new file mode 100644
index 00000000..4aaf4e04
--- /dev/null
+++ b/cryptonite-backport/Crypto/Error/Types.hs
@@ -0,0 +1,106 @@
1-- |
2-- Module : Crypto.Error.Types
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : stable
6-- Portability : Good
7--
8-- Cryptographic Error enumeration and handling
9--
10{-# LANGUAGE DeriveDataTypeable #-}
11module Crypto.Error.Types
12 ( CryptoError(..)
13 , CryptoFailable(..)
14 , throwCryptoErrorIO
15 , throwCryptoError
16 , onCryptoFailure
17 , eitherCryptoError
18 , maybeCryptoError
19 ) where
20
21import qualified Control.Exception as E
22import Data.Data
23
24import Crypto.Internal.Imports
25
26-- | Enumeration of all possible errors that can be found in this library
27data CryptoError =
28 -- symmetric cipher errors
29 CryptoError_KeySizeInvalid
30 | CryptoError_IvSizeInvalid
31 | CryptoError_AEADModeNotSupported
32 -- public key cryptography error
33 | CryptoError_SecretKeySizeInvalid
34 | CryptoError_SecretKeyStructureInvalid
35 | CryptoError_PublicKeySizeInvalid
36 | CryptoError_SharedSecretSizeInvalid
37 -- elliptic cryptography error
38 | CryptoError_EcScalarOutOfBounds
39 | CryptoError_PointSizeInvalid
40 | CryptoError_PointFormatInvalid
41 | CryptoError_PointFormatUnsupported
42 | CryptoError_PointCoordinatesInvalid
43 -- Message authentification error
44 | CryptoError_MacKeyInvalid
45 | CryptoError_AuthenticationTagSizeInvalid
46 deriving (Show,Eq,Enum,Data,Typeable)
47
48instance E.Exception CryptoError
49
50-- | A simple Either like type to represent a computation that can fail
51--
52-- 2 possibles values are:
53--
54-- * 'CryptoPassed' : The computation succeeded, and contains the result of the computation
55--
56-- * 'CryptoFailed' : The computation failed, and contains the cryptographic error associated
57--
58data CryptoFailable a =
59 CryptoPassed a
60 | CryptoFailed CryptoError
61 deriving (Show)
62
63instance Eq a => Eq (CryptoFailable a) where
64 (==) (CryptoPassed a) (CryptoPassed b) = a == b
65 (==) (CryptoFailed e1) (CryptoFailed e2) = e1 == e2
66 (==) _ _ = False
67
68instance Functor CryptoFailable where
69 fmap f (CryptoPassed a) = CryptoPassed (f a)
70 fmap _ (CryptoFailed r) = CryptoFailed r
71
72instance Applicative CryptoFailable where
73 pure a = CryptoPassed a
74 (<*>) fm m = fm >>= \p -> m >>= \r2 -> return (p r2)
75instance Monad CryptoFailable where
76 return a = CryptoPassed a
77 (>>=) m1 m2 = do
78 case m1 of
79 CryptoPassed a -> m2 a
80 CryptoFailed e -> CryptoFailed e
81
82-- | Throw an CryptoError as exception on CryptoFailed result,
83-- otherwise return the computed value
84throwCryptoErrorIO :: CryptoFailable a -> IO a
85throwCryptoErrorIO (CryptoFailed e) = E.throwIO e
86throwCryptoErrorIO (CryptoPassed r) = return r
87
88-- | Same as 'throwCryptoErrorIO' but throw the error asynchronously.
89throwCryptoError :: CryptoFailable a -> a
90throwCryptoError (CryptoFailed e) = E.throw e
91throwCryptoError (CryptoPassed r) = r
92
93-- | Simple 'either' like combinator for CryptoFailable type
94onCryptoFailure :: (CryptoError -> r) -> (a -> r) -> CryptoFailable a -> r
95onCryptoFailure onError _ (CryptoFailed e) = onError e
96onCryptoFailure _ onSuccess (CryptoPassed r) = onSuccess r
97
98-- | Transform a CryptoFailable to an Either
99eitherCryptoError :: CryptoFailable a -> Either CryptoError a
100eitherCryptoError (CryptoFailed e) = Left e
101eitherCryptoError (CryptoPassed a) = Right a
102
103-- | Transform a CryptoFailable to a Maybe
104maybeCryptoError :: CryptoFailable a -> Maybe a
105maybeCryptoError (CryptoFailed _) = Nothing
106maybeCryptoError (CryptoPassed r) = Just r
diff --git a/cryptonite-backport/Crypto/Internal/ByteArray.hs b/cryptonite-backport/Crypto/Internal/ByteArray.hs
new file mode 100644
index 00000000..3a23152d
--- /dev/null
+++ b/cryptonite-backport/Crypto/Internal/ByteArray.hs
@@ -0,0 +1,19 @@
1-- |
2-- Module : Crypto.Internal.ByteArray
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : stable
6-- Portability : Good
7--
8-- Simple and efficient byte array types
9--
10{-# OPTIONS_HADDOCK hide #-}
11module Crypto.Internal.ByteArray
12 ( module Data.ByteArray
13 , module Data.ByteArray.Mapping
14 , module Data.ByteArray.Encoding
15 ) where
16
17import Data.ByteArray
18import Data.ByteArray.Mapping
19import Data.ByteArray.Encoding
diff --git a/cryptonite-backport/Crypto/Internal/Compat.hs b/cryptonite-backport/Crypto/Internal/Compat.hs
new file mode 100644
index 00000000..a3712a7c
--- /dev/null
+++ b/cryptonite-backport/Crypto/Internal/Compat.hs
@@ -0,0 +1,48 @@
1-- |
2-- Module : Crypto.Internal.Compat
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : stable
6-- Portability : Good
7--
8-- This module try to keep all the difference between versions of base
9-- or other needed packages, so that modules don't need to use CPP
10--
11{-# LANGUAGE CPP #-}
12module Crypto.Internal.Compat
13 ( unsafeDoIO
14 , popCount
15 , byteSwap64
16 ) where
17
18import System.IO.Unsafe
19import Data.Word
20import Data.Bits
21
22-- | perform io for hashes that do allocation and ffi.
23-- unsafeDupablePerformIO is used when possible as the
24-- computation is pure and the output is directly linked
25-- to the input. we also do not modify anything after it has
26-- been returned to the user.
27unsafeDoIO :: IO a -> a
28#if __GLASGOW_HASKELL__ > 704
29unsafeDoIO = unsafeDupablePerformIO
30#else
31unsafeDoIO = unsafePerformIO
32#endif
33
34#if !(MIN_VERSION_base(4,5,0))
35popCount :: Word64 -> Int
36popCount n = loop 0 n
37 where loop c 0 = c
38 loop c i = loop (c + if testBit c 0 then 1 else 0) (i `shiftR` 1)
39#endif
40
41#if !(MIN_VERSION_base(4,7,0))
42byteSwap64 :: Word64 -> Word64
43byteSwap64 w =
44 (w `shiftR` 56) .|. (w `shiftL` 56)
45 .|. ((w `shiftR` 40) .&. 0xff00) .|. ((w .&. 0xff00) `shiftL` 40)
46 .|. ((w `shiftR` 24) .&. 0xff0000) .|. ((w .&. 0xff0000) `shiftL` 24)
47 .|. ((w `shiftR` 8) .&. 0xff000000) .|. ((w .&. 0xff000000) `shiftL` 8)
48#endif
diff --git a/cryptonite-backport/Crypto/Internal/DeepSeq.hs b/cryptonite-backport/Crypto/Internal/DeepSeq.hs
new file mode 100644
index 00000000..9da79881
--- /dev/null
+++ b/cryptonite-backport/Crypto/Internal/DeepSeq.hs
@@ -0,0 +1,33 @@
1-- |
2-- Module : Crypto.Internal.DeepSeq
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : experimental
6-- Portability : unknown
7--
8-- Simple abstraction module to allow compilation without deepseq
9-- by defining our own NFData class if not compiling with deepseq
10-- support.
11--
12{-# LANGUAGE CPP #-}
13module Crypto.Internal.DeepSeq
14 ( NFData(..)
15 ) where
16
17#ifdef WITH_DEEPSEQ_SUPPORT
18import Control.DeepSeq
19#else
20import Data.Word
21import Data.ByteArray
22
23class NFData a where rnf :: a -> ()
24
25instance NFData Word8 where rnf w = w `seq` ()
26instance NFData Word16 where rnf w = w `seq` ()
27instance NFData Word32 where rnf w = w `seq` ()
28instance NFData Word64 where rnf w = w `seq` ()
29
30instance NFData Bytes where rnf b = b `seq` ()
31instance NFData ScrubbedBytes where rnf b = b `seq` ()
32
33#endif
diff --git a/cryptonite-backport/Crypto/Internal/Imports.hs b/cryptonite-backport/Crypto/Internal/Imports.hs
new file mode 100644
index 00000000..4ed44e16
--- /dev/null
+++ b/cryptonite-backport/Crypto/Internal/Imports.hs
@@ -0,0 +1,16 @@
1-- |
2-- Module : Crypto.Internal.Imports
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : experimental
6-- Portability : unknown
7--
8module Crypto.Internal.Imports
9 ( module X
10 ) where
11
12import Data.Word as X
13import Control.Applicative as X
14import Control.Monad as X (forM, forM_, void)
15import Control.Arrow as X (first, second)
16import Crypto.Internal.DeepSeq as X
diff --git a/cryptonite-backport/Crypto/PubKey/Curve25519.hs b/cryptonite-backport/Crypto/PubKey/Curve25519.hs
new file mode 100644
index 00000000..42878691
--- /dev/null
+++ b/cryptonite-backport/Crypto/PubKey/Curve25519.hs
@@ -0,0 +1,131 @@
1-- |
2-- Module : Crypto.PubKey.Curve25519
3-- License : BSD-style
4-- Maintainer : Vincent Hanquez <vincent@snarc.org>
5-- Stability : experimental
6-- Portability : unknown
7--
8-- Curve25519 support
9--
10{-# LANGUAGE GeneralizedNewtypeDeriving #-}
11{-# LANGUAGE MagicHash #-}
12{-# LANGUAGE ScopedTypeVariables #-}
13module Crypto.PubKey.Curve25519
14 ( SecretKey
15 , PublicKey
16 , DhSecret
17 -- * Smart constructors
18 , dhSecret
19 , publicKey
20 , secretKey
21 -- * methods
22 , dh
23 , toPublic
24 , generateSecretKey
25 ) where
26
27import Data.Bits
28import Data.Word
29import Foreign.Ptr
30import Foreign.Storable
31import GHC.Ptr
32
33-- import Crypto.Error
34import Crypto.Error.Types
35import Crypto.Internal.Compat
36import Crypto.Internal.Imports
37import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, ScrubbedBytes, Bytes, withByteArray)
38import qualified Crypto.Internal.ByteArray as B
39-- import Crypto.Error (CryptoFailable(..))
40import Crypto.Random
41
42-- | A Curve25519 Secret key
43newtype SecretKey = SecretKey ScrubbedBytes
44 deriving (Show,Eq,ByteArrayAccess,NFData)
45
46-- | A Curve25519 public key
47newtype PublicKey = PublicKey Bytes
48 deriving (Show,Eq,ByteArrayAccess,NFData)
49
50-- | A Curve25519 Diffie Hellman secret related to a
51-- public key and a secret key.
52newtype DhSecret = DhSecret ScrubbedBytes
53 deriving (Show,Eq,ByteArrayAccess,NFData)
54
55-- | Try to build a public key from a bytearray
56publicKey :: ByteArrayAccess bs => bs -> CryptoFailable PublicKey
57publicKey bs
58 | B.length bs == 32 = CryptoPassed $ PublicKey $ B.copyAndFreeze bs (\_ -> return ())
59 | otherwise = CryptoFailed CryptoError_PublicKeySizeInvalid
60
61-- | Try to build a secret key from a bytearray
62secretKey :: ByteArrayAccess bs => bs -> CryptoFailable SecretKey
63secretKey bs
64 | B.length bs == 32 = unsafeDoIO $ do
65 withByteArray bs $ \inp -> do
66 valid <- isValidPtr inp
67 if valid
68 then (CryptoPassed . SecretKey) <$> B.copy bs (\_ -> return ())
69 else return $ CryptoFailed CryptoError_SecretKeyStructureInvalid
70 | otherwise = CryptoFailed CryptoError_SecretKeySizeInvalid
71 where
72 -- e[0] &= 0xf8;
73 -- e[31] &= 0x7f;
74 -- e[31] |= 40;
75 isValidPtr :: Ptr Word8 -> IO Bool
76 isValidPtr _ = do
77 --b0 <- peekElemOff inp 0
78 --b31 <- peekElemOff inp 31
79 return True
80{-
81 return $ and [ testBit b0 0 == False
82 , testBit b0 1 == False
83 , testBit b0 2 == False
84 , testBit b31 7 == False
85 , testBit b31 6 == True
86 ]
87-}
88{-# NOINLINE secretKey #-}
89
90-- | Create a DhSecret from a bytearray object
91dhSecret :: ByteArrayAccess b => b -> CryptoFailable DhSecret
92dhSecret bs
93 | B.length bs == 32 = CryptoPassed $ DhSecret $ B.copyAndFreeze bs (\_ -> return ())
94 | otherwise = CryptoFailed CryptoError_SharedSecretSizeInvalid
95
96-- | Compute the Diffie Hellman secret from a public key and a secret key
97dh :: PublicKey -> SecretKey -> DhSecret
98dh (PublicKey pub) (SecretKey sec) = DhSecret <$>
99 B.allocAndFreeze 32 $ \result ->
100 withByteArray sec $ \psec ->
101 withByteArray pub $ \ppub ->
102 ccryptonite_curve25519 result psec ppub
103{-# NOINLINE dh #-}
104
105-- | Create a public key from a secret key
106toPublic :: SecretKey -> PublicKey
107toPublic (SecretKey sec) = PublicKey <$>
108 B.allocAndFreeze 32 $ \result ->
109 withByteArray sec $ \psec ->
110 ccryptonite_curve25519 result psec basePoint
111 where
112 basePoint = Ptr "\x09\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"#
113{-# NOINLINE toPublic #-}
114
115-- | Generate a secret key.
116generateSecretKey :: MonadRandom m => m SecretKey
117generateSecretKey = tweakToSecretKey <$> getRandomBytes 32
118 where
119 tweakToSecretKey :: ScrubbedBytes -> SecretKey
120 tweakToSecretKey bin = SecretKey $ B.copyAndFreeze bin $ \inp -> do
121 modifyByte inp 0 (\e0 -> e0 .&. 0xf8)
122 modifyByte inp 31 (\e31 -> (e31 .&. 0x7f) .|. 0x40)
123
124 modifyByte :: Ptr Word8 -> Int -> (Word8 -> Word8) -> IO ()
125 modifyByte p n f = peekByteOff p n >>= pokeByteOff p n . f
126
127foreign import ccall "cryptonite_curve25519_donna"
128 ccryptonite_curve25519 :: Ptr Word8 -- ^ public
129 -> Ptr Word8 -- ^ secret
130 -> Ptr Word8 -- ^ basepoint
131 -> IO ()
diff --git a/dht-client.cabal b/dht-client.cabal
new file mode 100644
index 00000000..7c3dcdc3
--- /dev/null
+++ b/dht-client.cabal
@@ -0,0 +1,213 @@
1name: dht-client
2version: 0.0.0.4
3license: BSD3
4license-file: LICENSE
5author: Joe Crayne
6maintainer: Joe Crayne
7copyright: (c) 2017 Joe Crayne, (c) 2013, Sam Truzjan
8category: Network
9build-type: Custom
10cabal-version: >= 1.10
11tested-with: GHC == 8.0.2, GHC == 7.10.3
12homepage: https://github.com/cobit/bittorrent
13bug-reports: https://github.com/cobit/bittorrent/issues
14synopsis: BitTorrent DHT protocol implementation.
15description:
16
17 A library for making Haskell bittorrent applications easy.
18 .
19 For more information see:
20 <https://github.com/cobit/bittorrent/blob/master/README.md>
21
22extra-source-files: res/dapper-dvd-amd64.iso.torrent
23 res/pkg.torrent
24 README.md
25 ChangeLog
26 cbits/*.h
27
28
29source-repository head
30 type: git
31 location: git://github.com/cobit/bittorrent.git
32
33source-repository this
34 type: git
35 location: git://github.com/cobit/bittorrent.git
36 branch: master
37 tag: v0.0.0.4
38
39flag network-uri
40 description: Use network-uri package.
41 default: True
42
43flag builder
44 description: Use older bytestring package and bytestring-builder.
45 default: False
46
47-- flag aeson
48-- description: Use aeson for pretty-printing bencoded data.
49-- default: True
50
51flag thread-debug
52 description: Add instrumentation to threads.
53 default: True
54
55library
56 default-language: Haskell2010
57 default-extensions: PatternGuards
58 , OverloadedStrings
59 , RecordWildCards
60 , NondecreasingIndentation
61 hs-source-dirs: src, cryptonite-backport, .
62 exposed-modules: Network.SocketLike
63 Data.Digest.CRC32C
64 Data.Bits.ByteString
65 Data.Wrapper.PSQ
66 Data.Wrapper.PSQInt
67 Data.MinMaxPSQ
68 Network.Address
69 Network.Kademlia.Bootstrap
70 Network.Kademlia.Routing
71 Data.Torrent
72 Network.BitTorrent.DHT.ContactInfo
73 Network.BitTorrent.DHT.Token
74 Network.Kademlia.Search
75 Network.QueryResponse
76 Network.StreamServer
77 Data.BEncode.Pretty
78 Control.Concurrent.Tasks
79 Network.Kademlia
80 Network.BitTorrent.MainlineDHT
81 Network.BitTorrent.MainlineDHT.Symbols
82 System.Global6
83 Data.Word64Map
84 OnionRouter
85 Network.Tox
86 Network.Tox.Transport
87 Network.Tox.Crypto.Transport
88 Network.Tox.Crypto.Handlers
89 Network.Tox.Onion.Handlers
90 Network.Tox.Onion.Transport
91 Network.Tox.DHT.Handlers
92 Network.Tox.DHT.Transport
93 Network.Tox.NodeId
94 Network.UPNP
95 Control.TriadCommittee
96 Crypto.Tox
97 Text.XXD
98 Roster
99 Announcer
100 InterruptibleDelay
101
102 build-depends: base
103 , containers
104 , array
105 , hashable
106 , iproute
107 , stm
108 , base16-bytestring
109 , base32-bytestring
110 , base64-bytestring
111 , psqueues
112 , reflection
113 , deepseq
114 , text
115 , filepath
116 , directory
117 , bencoding
118 , contravariant
119
120 , cryptonite
121 , memory
122 , time
123 , random
124 , entropy
125 , cpu
126
127 , cereal
128 , http-types
129
130 , process
131 , split
132 , pretty
133 , convertible
134 , data-default
135
136 , bifunctors
137 , lens
138 , lifted-async
139 , lifted-base
140 , monad-control
141 , transformers-base
142 , mtl
143 , ghc-prim
144
145 if impl(ghc < 8)
146 Build-depends: transformers
147
148 if flag(network-uri)
149 Build-depends: network >= 2.6
150 , network-uri >= 2.6
151 else
152 Build-depends: network >= 2.4 && < 2.6
153
154
155 other-modules: Paths_dht_client
156 Crypto.Cipher.Salsa
157 Crypto.Cipher.XSalsa
158 Crypto.ECC.Class
159 Crypto.ECC.Simple.Prim
160 Crypto.ECC.Simple.Types
161 Crypto.Error.Types
162 Crypto.Internal.ByteArray
163 Crypto.Internal.Compat
164 Crypto.Internal.DeepSeq
165 Crypto.Internal.Imports
166 Crypto.PubKey.Curve25519
167
168 C-sources: cbits/cryptonite_xsalsa.c, cbits/cryptonite_salsa.c
169
170 -- if flag(aeson)
171 build-depends: aeson, aeson-pretty, unordered-containers, vector
172 cpp-options: -DBENCODE_AESON
173 if flag(thread-debug)
174 exposed-modules: Control.Concurrent.Lifted.Instrument
175 Control.Concurrent.Async.Lifted.Instrument
176 cpp-options: -DTHREAD_DEBUG
177
178 if flag(builder)
179 build-depends: bytestring >= 0.9, bytestring-builder
180 else
181 build-depends: bytestring >= 0.10
182 ghc-options: -fwarn-missing-signatures -fdefer-typed-holes
183 ghc-prof-options:
184
185
186executable dht
187 hs-source-dirs: examples
188 main-is: dht.hs
189 default-language: Haskell2010
190 build-depends: base, haskeline, network, bytestring, transformers
191
192executable dhtd
193 hs-source-dirs: examples
194 main-is: dhtd.hs
195 default-language: Haskell2010
196 build-depends: base, network, bytestring, hashable, deepseq
197 , aeson
198 , pretty
199 , dht-client
200 , unix
201 , containers
202 , stm
203 , cereal
204 , bencoding
205 , unordered-containers
206 , vector
207 , text
208
209 if flag(thread-debug)
210 build-depends: time
211 cpp-options: -DTHREAD_DEBUG
212 ghc-options: -rtsopts -fdefer-typed-holes
213
diff --git a/examples/dht.hs b/examples/dht.hs
new file mode 100644
index 00000000..3e1b1656
--- /dev/null
+++ b/examples/dht.hs
@@ -0,0 +1,90 @@
1{-# LANGUAGE NondecreasingIndentation #-}
2import Control.Applicative
3import Control.Monad
4import Data.Function
5import Control.Monad.IO.Class
6import Data.Char
7import Data.List
8import Network.Socket as Socket
9import System.Console.Haskeline
10import System.Environment
11import System.Exit
12import System.IO
13import System.IO.Unsafe
14import qualified Data.ByteString as B
15
16-- | Reads one character. If it is not a digit,
17-- then it is discarded and 'Nothing' is returned.
18hReadDigit :: Handle -> IO (Maybe Char)
19hReadDigit h = do c <- hGetChar h
20 return $ guard (isDigit c) >> pure c
21
22-- | Expected input: "nnn:..."
23-- Here we read the digit sequence "nnn" and drop the colon
24-- as it is the first non-digit.
25hReadInt :: Handle -> IO Int
26hReadInt h = do
27 nstr <- fix $ \readDigits ->
28 maybe (return []) -- dropped non-digit character
29 (($ unsafeInterleaveIO readDigits) . fmap . (:))
30 =<< hReadDigit h
31 readIO nstr :: IO Int
32
33
34-- | Read a length prefixed string from a handle.
35-- The format is "nnn:..." where /nnn/ is an ascii-encoded character count
36-- and /.../ is the sequence of characters
37--
38-- Note: The first byte after the count is ignored and discarded.
39readResponse :: Handle -> IO (Char, String)
40readResponse h = do
41 c <- hGetChar h
42 n <- hReadInt h
43 s <- sequence $ replicate n (hGetChar h)
44 return (c,s)
45
46-- | Send a command to the dhtd daemon and then print the response.
47sendCommand :: Handle -> String -> InputT IO ()
48sendCommand h cmd = do liftIO $ hPutStrLn h cmd
49 fix $ \again -> do
50 (c, resp) <- liftIO $ readResponse h
51 if c /= '.'
52 then outputStr resp >> again
53 else outputStrLn resp
54
55-- | Get one line of input and send it to the daemon, then run the
56-- passed continuation if it wasn't "quit".
57interactiveMode :: Handle -> InputT IO () -> InputT IO ()
58interactiveMode h repl = do
59 minput <- getInputLine "dht> "
60 case minput of
61 Nothing -> return ()
62 Just "quit" -> sendCommand h "quit" >> return ()
63 Just cmd -> sendCommand h cmd >> repl
64
65main :: IO ()
66main = do
67 -- Open the control socket to the daemon.
68 h <- liftIO $ handle (\e -> do hPutStrLn stderr (show (e ::IOError))
69 exitFailure)
70 $ do sock <- socket AF_UNIX Stream defaultProtocol
71 connect sock (SockAddrUnix "dht.sock")
72 socketToHandle sock ReadWriteMode
73
74 -- Haskeline's default looks only at our stdin and not our stdout.
75 -- That's a bad idea because we can take input from the command line.
76 behavior <- do
77 useTerminal <- and <$> mapM hIsTerminalDevice [stdin,stdout]
78 return $ if useTerminal then preferTerm else useFileHandle stdin
79
80 runInputTBehaviorWithPrefs behavior defaultPrefs defaultSettings $ do
81
82 -- A command may be specified on the command line
83 -- or else we enter an interactive shell.
84 args <- dropWhile isSpace . unwords <$> liftIO getArgs
85 case args of
86 (_:_) -> do
87 let cs = filter (not . null) $ map (drop 1) $ groupBy (\_ c -> (c/=';')) (';':args)
88 forM_ cs $ \cmd -> sendCommand h cmd
89 sendCommand h "quit"
90 _ -> fix $ interactiveMode h
diff --git a/examples/dhtd.hs b/examples/dhtd.hs
new file mode 100644
index 00000000..837cb210
--- /dev/null
+++ b/examples/dhtd.hs
@@ -0,0 +1,1341 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE ExistentialQuantification #-}
3{-# LANGUAGE FlexibleContexts #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE LambdaCase #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE NamedFieldPuns #-}
8{-# LANGUAGE NondecreasingIndentation #-}
9{-# LANGUAGE OverloadedStrings #-}
10{-# LANGUAGE PartialTypeSignatures #-}
11{-# LANGUAGE PatternSynonyms #-}
12{-# LANGUAGE RankNTypes #-}
13{-# LANGUAGE RecordWildCards #-}
14{-# LANGUAGE ScopedTypeVariables #-}
15{-# LANGUAGE TupleSections #-}
16{-# LANGUAGE TypeFamilies #-}
17{-# LANGUAGE TypeOperators #-}
18
19module Main where
20
21import Control.Arrow
22import Control.Applicative
23import Control.Concurrent.STM
24import Control.DeepSeq
25import Control.Exception
26import Control.Monad
27import Data.Bool
28import Data.Char
29import Data.Hashable
30import Data.List
31import qualified Data.IntMap.Strict as IntMap
32import qualified Data.Map.Strict as Map
33import Data.Maybe
34import qualified Data.Set as Set
35import Data.Time.Clock
36import GHC.Conc (threadStatus,ThreadStatus(..))
37import GHC.Stats
38import Network.Socket
39import System.Environment
40import System.IO
41import System.Mem
42import System.Posix.Process
43import Text.PrettyPrint.HughesPJClass
44import Text.Printf
45import Text.Read
46#ifdef THREAD_DEBUG
47import Control.Concurrent.Lifted.Instrument
48#else
49import Control.Concurrent.Lifted
50import GHC.Conc (labelThread)
51#endif
52import qualified Data.HashMap.Strict as HashMap
53import qualified Data.Vector as V
54import qualified Data.Text as T
55import qualified Data.Text.Encoding as T
56
57import Announcer
58import Crypto.Tox -- (zeros32,SecretKey,PublicKey, generateSecretKey, toPublic, encodeSecret, decodeSecret, userKeys)
59import Network.UPNP as UPNP
60import Network.Address hiding (NodeId, NodeInfo(..))
61import Network.QueryResponse
62import Network.StreamServer
63import Network.Kademlia
64import Network.Kademlia.Bootstrap
65import Network.Kademlia.Search
66import qualified Network.BitTorrent.MainlineDHT as Mainline
67import qualified Network.Tox as Tox
68import Network.Kademlia.Routing as R
69import Data.Aeson as J (ToJSON, FromJSON)
70import qualified Data.Aeson as J
71import qualified Data.ByteString.Lazy as L
72import qualified Data.ByteString.Char8 as B
73import Control.Concurrent.Tasks
74import System.IO.Error
75import qualified Data.Serialize as S
76import Network.BitTorrent.DHT.ContactInfo as Peers
77import qualified Data.MinMaxPSQ as MM
78import Data.Wrapper.PSQ as PSQ (pattern (:->))
79import qualified Data.Wrapper.PSQ as PSQ
80import Data.Ord
81import Data.Time.Clock.POSIX
82import qualified Network.Tox.DHT.Transport as Tox
83import qualified Network.Tox.DHT.Handlers as Tox
84import qualified Network.Tox.Onion.Transport as Tox
85import qualified Network.Tox.Onion.Handlers as Tox
86import qualified Network.Tox.Crypto.Handlers as Tox
87import Data.Typeable
88import Roster
89import OnionRouter
90
91showReport :: [(String,String)] -> String
92showReport kvs = showColumns $ map (\(x,y)->[x,y]) kvs
93
94showColumns :: [[String]] -> String
95showColumns rows = do
96 let cols = transpose rows
97 ws = map (maximum . map (succ . length)) cols
98 fs <- rows
99 _ <- take 1 fs -- Guard against empty rows so that 'last' is safe.
100 " " ++ concat (zipWith (printf "%-*s") (init ws) (init fs)) ++ last fs ++ "\n"
101
102
103marshalForClient :: String -> String
104marshalForClient s = show (length s) ++ ":" ++ s
105
106data ClientHandle = ClientHandle Handle (MVar Int)
107
108-- | Writes a message and signals ready for next command.
109hPutClient :: ClientHandle -> String -> IO ()
110hPutClient (ClientHandle h hstate) s = do
111 st <- takeMVar hstate
112 hPutStr h ('.' : marshalForClient s)
113 putMVar hstate 1 -- ready for input
114
115-- | Writes message, but signals there is more to come.
116hPutClientChunk :: ClientHandle -> String -> IO ()
117hPutClientChunk (ClientHandle h hstate) s = do
118 st <- takeMVar hstate
119 hPutStr h (' ' : marshalForClient s)
120 putMVar hstate 2 -- ready for more output
121
122data DHTQuery nid ni = forall addr r tok.
123 ( Ord addr
124 , Typeable r
125 , Typeable tok
126 , Typeable ni
127 ) => DHTQuery
128 { qsearch :: Search nid addr tok ni r
129 , qhandler :: ni -> nid -> IO ([ni], [r], Maybe tok) -- ^ Invoked on local node, when there is no query destination.
130 , qshowR :: r -> String
131 , qshowTok :: tok -> Maybe String
132 }
133
134data DHTAnnouncable nid = forall dta tok ni r.
135 ( Show r
136 , Typeable dta
137 , Typeable tok
138 , Typeable r
139 , Typeable ni
140 ) => DHTAnnouncable
141 { announceParseData :: String -> Either String dta
142 , announceParseToken :: dta -> String -> Either String tok
143 , announceParseAddress :: String -> Either String ni
144 , announceSendData :: dta -> tok -> Maybe ni -> IO (Maybe r)
145 , announceInterval :: POSIXTime
146 , qresultAddr :: dta -> nid
147 }
148
149data DHTLink = forall status linkid params.
150 ( Show status
151 , Show linkid
152 , Typeable status
153 , Typeable linkid
154 , Typeable params
155 ) => DHTLink
156 { linkInit :: params -> IO (Either String status)
157 , linkParamParser :: [String] -> Either String params
158 , linkStatus :: IO (Either String status)
159 , showLinkStatus :: status -> String
160 , linkNewPipe :: String -> linkid -> IO (Either String status)
161 , linkUnPipe :: linkid -> IO (Either String status)
162 }
163
164data DHTSearch nid ni = forall addr tok r. DHTSearch
165 { searchThread :: ThreadId
166 , searchState :: SearchState nid addr tok ni r
167 , searchShowTok :: tok -> Maybe String
168 , searchResults :: TVar (Set.Set String)
169 }
170
171data DHTPing ni = forall r. DHTPing
172 { pingQuery :: [String] -> ni -> IO (Maybe r)
173 , pingShowResult :: r -> String
174 }
175
176data DHT = forall nid ni. ( Show ni
177 , Read ni
178 , ToJSON ni
179 , FromJSON ni
180 , Ord ni
181 , Hashable ni
182 , Show nid
183 , Ord nid
184 , Hashable nid
185 , Typeable ni
186 , S.Serialize nid
187 ) =>
188 DHT
189 { dhtBuckets :: TVar (BucketList ni)
190 , dhtSecretKey :: STM (Maybe SecretKey)
191 , dhtPing :: Map.Map String (DHTPing ni)
192 , dhtQuery :: Map.Map String (DHTQuery nid ni)
193 , dhtAnnouncables :: Map.Map String (DHTAnnouncable nid)
194 , dhtLinks :: Map.Map String DHTLink
195 , dhtParseId :: String -> Either String nid
196 , dhtSearches :: TVar (Map.Map (String,nid) (DHTSearch nid ni))
197 , dhtFallbackNodes :: IO [ni]
198 , dhtBootstrap :: [ni] -> [ni] -> IO ()
199 }
200
201nodesFileName :: String -> String
202nodesFileName netname = netname ++ "-nodes.json"
203
204saveNodes :: String -> DHT -> IO ()
205saveNodes netname DHT{dhtBuckets} = do
206 bkts <- atomically $ readTVar dhtBuckets
207 let ns = map fst $ concat $ R.toList bkts
208 bs = J.encode ns
209 fname = nodesFileName netname
210 L.writeFile fname bs
211
212loadNodes :: FromJSON ni => String -> IO [ni]
213loadNodes netname = do
214 let fname = nodesFileName netname
215 attempt <- tryIOError $ do
216 J.decode <$> L.readFile fname
217 >>= maybe (ioError $ userError "Nothing") return
218 either (const $ fallbackLoad fname) return attempt
219
220fallbackLoad :: FromJSON t => FilePath -> IO [t]
221fallbackLoad fname = do
222 attempt <- tryIOError $ do
223 J.decode <$> L.readFile fname
224 >>= maybe (ioError $ userError "Nothing") return
225 let go r = do
226 let m = HashMap.lookup "nodes" (r :: J.Object)
227 ns0 = case m of Just (J.Array v) -> V.toList v
228 Nothing -> []
229 ns1 = zip (map J.fromJSON ns0) ns0
230 issuc (J.Error _,_) = False
231 issuc _ = True
232 (ss,fs) = partition issuc ns1
233 ns = map (\(J.Success n,_) -> n) ss
234 mapM_ print (map snd fs) >> return ns
235 either (const $ return []) go attempt
236
237
238{-
239pingNodes :: String -> DHT -> IO Bool
240pingNodes netname DHT{dhtPing} | Just DHTPing{pingQuery=ping} <- Map.lookup "ping" dhtPing = do
241 let fname = nodesFileName netname
242 attempt <- tryIOError $ do
243 J.decode <$> L.readFile fname
244 >>= maybe (ioError $ userError "Nothing") return
245-}
246
247asProxyTypeOf :: a -> proxy a -> a
248asProxyTypeOf = const
249
250pingNodes :: String -> DHT -> IO (Maybe Int)
251pingNodes netname dht@DHT{dhtPing} | Just DHTPing{pingQuery=ping} <- Map.lookup "ping" dhtPing = do
252 let fname = nodesFileName netname
253 ns <- loadNodes netname
254 case ns of
255 [] -> return Nothing
256 _ -> do
257 fork $ do
258 myThreadId >>= flip labelThread ("pinging."++fname)
259 putStrLn $ "Forked "++show fname
260 withTaskGroup ("withTaskGroup."++fname) 10 $ \g -> do
261 forM_ (ns `asTypeOf` [])
262 $ \n -> forkTask g (show n)
263 $ void
264 $ ping [] n
265 putStrLn $ "Load finished "++show fname
266 return $ Just $ length ns
267pingNodes _ _ = return Nothing
268
269
270
271reportTable :: Show ni => BucketList ni -> [(String,String)]
272reportTable bkts = map (show *** show . fst)
273 $ concat
274 $ zipWith map (map (,) [0::Int ..])
275 $ R.toList
276 $ bkts
277
278reportResult ::
279 String
280 -> (r -> String)
281 -> (tok -> Maybe String)
282 -> (ni -> String)
283 -> ClientHandle
284 -> Either String ([ni],[r],Maybe tok)
285 -> IO ()
286reportResult meth showR showTok showN h (Left e) = hPutClient h e
287reportResult meth showR showTok showN h (Right (ns,rs,tok)) = do
288 hPutClient h $ showReport report
289 where
290 report = intercalate [("","")] [ tok_r , node_r , result_r ]
291
292 tok_r = maybe [] (pure . ("token:",)) $ showTok =<< tok
293
294 node_r = map ( ("n",) . showN ) ns
295
296 result_r | (meth=="node") = []
297 | otherwise = map ( (take 1 meth,) . showR ) rs
298
299-- example:
300-- * 10 peer 141d6c6ee2810f46d28bbe8373d4f454a4122535
301-- - 1 peer 141d6c6ee2810f46d28bbe8373d4f454a4122535
302-- 22 node 141d6c6ee2810f46d28bbe8373d4f454a4122535
303--
304-- key: '*' in progress
305-- '-' stopped
306-- ' ' finished
307showSearches :: ( Show nid
308 , Ord nid
309 , Hashable nid
310 , Ord ni
311 , Hashable ni
312 ) => Map.Map (String,nid) (DHTSearch nid ni) -> IO String
313showSearches searches = do
314 tups <- forM (Map.toList searches) $ \((meth,nid),DHTSearch{..}) -> do
315 (is'fin, cnt) <- atomically $
316 (,) <$> searchIsFinished searchState
317 <*> (Set.size <$> readTVar searchResults)
318 tstat <- threadStatus searchThread
319 let stat = case tstat of
320 _ | is'fin -> ' '
321 ThreadFinished -> '-'
322 ThreadDied -> '-'
323 _ -> '*'
324 return (stat,show cnt,meth,show nid)
325 let cnt'width = maximum $ map (\(_,cnt,_,_)->length cnt) tups
326 mth'width = maximum $ map (\(_,_,mth,_)->length mth) tups
327 return $ do -- List monad.
328 (stat,cnt,meth,nid) <- tups
329 printf " %c %-*s %-*s %s\n" stat cnt'width cnt mth'width meth nid
330
331forkSearch ::
332 ( Ord nid
333 , Hashable nid
334 , Ord ni
335 , Hashable ni
336 , Show nid
337 ) =>
338 String
339 -> nid
340 -> DHTQuery nid ni
341 -> TVar (Map.Map (String,nid) (DHTSearch nid ni))
342 -> TVar (BucketList ni)
343 -> ThreadId
344 -> TVar (Maybe (IO ()))
345 -> STM ()
346forkSearch method nid DHTQuery{qsearch,qshowTok,qshowR} dhtSearches dhtBuckets tid kvar = do
347 ns <- R.kclosest (searchSpace qsearch) searchK nid <$> readTVar dhtBuckets
348 st <- newSearch qsearch nid ns
349 results <- newTVar Set.empty
350 let storeResult r = modifyTVar' results (Set.insert (qshowR r))
351 >> return True
352 new = DHTSearch
353 { searchThread = tid
354 , searchState = st
355 , searchShowTok = qshowTok
356 , searchResults = results
357 }
358 modifyTVar' dhtSearches $ Map.insert (method,nid) new
359 -- Finally, we write the search loop action into a tvar that will be executed in a new
360 -- thread.
361 writeTVar kvar $ Just $ searchLoop qsearch nid storeResult st
362
363reportSearchResults :: (Show t, Ord t1, Ord t, Hashable t) =>
364 String -> ClientHandle -> DHTSearch t1 t -> IO ()
365reportSearchResults meth h DHTSearch{searchShowTok,searchState,searchResults} = do
366 (ns,rs) <- atomically $ do
367 mm <- readTVar $ searchInformant searchState
368 rset <- readTVar searchResults
369 let ns = map (\(MM.Binding ni tok _) -> (ni,tok))
370 $ MM.toList mm
371 rs = Set.toList rset
372 return (ns,rs)
373 let n'width = succ $ maximum $ map (length . show . fst) ns
374 showN (n,tok) = take n'width (show n ++ repeat ' ') ++ (fromMaybe "" $ searchShowTok =<< tok)
375 ns' = map showN ns
376 reportResult meth id (const Nothing) id h (Right (ns',rs, Just ()))
377
378data Session = Session
379 { netname :: String
380 , dhts :: Map.Map String DHT
381 , externalAddresses :: IO [SockAddr]
382 , swarms :: Mainline.SwarmsDatabase
383 , cryptosessions :: Tox.NetCryptoSessions
384 , toxkeys :: TVar Tox.AnnouncedKeys
385 , userkeys :: TVar [(SecretKey,PublicKey)]
386 , roster :: Roster
387 , onionRouter :: OnionRouter
388 , announcer :: Announcer
389 , signalQuit :: MVar ()
390 }
391
392exceptionsToClient :: ClientHandle -> IO () -> IO ()
393exceptionsToClient (ClientHandle h hstate) action =
394 action `catch` \(SomeException e) -> do
395 st <- takeMVar hstate
396 when (st /= 1) $ do
397 hPutStr h ('.': marshalForClient (show e))
398 putMVar hstate 1 -- ready for input
399
400hGetClientLine :: ClientHandle -> IO String
401hGetClientLine (ClientHandle h hstate) = do
402 st <- takeMVar hstate
403 -- st should be 1
404 x <- hGetLine h
405 putMVar hstate 0 -- ready for output
406 return x
407
408hCloseClient :: ClientHandle -> IO ()
409hCloseClient (ClientHandle h hstate) = do
410 st <- takeMVar hstate
411 hClose h
412 putMVar hstate 3 -- closed file handle
413
414clientSession0 :: Session -> t1 -> t -> Handle -> IO ()
415clientSession0 s sock cnum h = do
416 hstate <- newMVar 1 -- ready for input
417 clientSession s sock cnum (ClientHandle h hstate)
418 `catch` \e -> if isEOFError e then return ()
419 else throwIO e
420
421clientSession :: Session -> t1 -> t -> ClientHandle -> IO ()
422clientSession s@Session{..} sock cnum h = do
423 line <- dropWhile isSpace <$> hGetClientLine h
424 let (c,args) = second (dropWhile isSpace) $ break isSpace line
425 cmd0 :: IO () -> IO ()
426 cmd0 action = exceptionsToClient h action >> clientSession s sock cnum h
427 switchNetwork dest = do hPutClient h ("Network: "++dest)
428 clientSession s{netname=dest} sock cnum h
429 strp = B.unpack . fst . until snd dropEnd . (,False) . B.dropWhile isSpace . B.pack
430 where
431 dropEnd (x,_) =
432 case B.unsnoc x of
433 Just (str,c) | isSpace c -> (str,False)
434 _ -> (x,True)
435 let mkrow :: (SecretKey, PublicKey) -> (String,String)
436 mkrow (a,b) | Just x <- encodeSecret a= (B.unpack x, show (Tox.key2id b))
437 mkrow _ = error (concat ["Assertion fail in 'mkrow' function at ", __FILE__, ":", show __LINE__])
438 sessionCommands :: [[String]]
439 sessionCommands =
440 [ ["stop"]
441 , ["quit"]
442 , ["pid"]
443 , ["external-ip"]
444 , ["threads"]
445 , ["mem"]
446 , ["ls"]
447 , ["k"]
448 , ["roster"]
449 , ["onion"]
450 , ["g"]
451 , ["p"]
452 , ["a"]
453 , ["s"]
454 , ["x"]
455 , ["save"]
456 , ["load"]
457 , ["swarms"]
458 , ["peers"]
459 , ["toxids"]
460 , ["c"]
461 , ["help"]
462 , ["throw"]
463 ]
464 case (map toLower c,args) of
465 (n, _) | n `elem` Map.keys dhts -> switchNetwork n
466 -- "ping"
467 -- "cookie"
468 (pinglike, s) | Just DHT{dhtPing} <- Map.lookup netname dhts
469 , Just DHTPing{ pingQuery=ping
470 , pingShowResult=showr } <- Map.lookup pinglike dhtPing
471 , ws@(_:_) <- words s
472 -> cmd0 $ do
473 case readEither $ last ws of
474 Right addr -> do result <- ping (init ws) addr
475 let rs = [" ", maybe "Timeout." showr result]
476 hPutClient h $ unlines rs
477 Left er -> hPutClient h er
478 (x,_) | not (null (strp x))
479 , x `notElem` map head sessionCommands -> cmd0 $ do
480 hPutClient h $ "error."
481
482 ("stop", _) -> do hPutClient h "Terminating DHT Daemon."
483 hCloseClient h
484 putMVar signalQuit ()
485
486 ("throw", er) -> cmd0 $ do
487 throwIO $ userError er
488 hPutClient h "The impossible happened!"
489
490 ("quit", _) -> hPutClient h "" >> hCloseClient h
491
492 ("pid", _) -> cmd0 $ do
493 pid <- getProcessID
494 hPutClient h (show pid)
495 ("external-ip", _) -> cmd0 $ do
496 unlines . map (either show show . either4or6) <$> externalAddresses
497 >>= hPutClient h
498#ifdef THREAD_DEBUG
499 ("threads", _) -> cmd0 $ do
500 ts <- threadsInformation
501 tm <- getCurrentTime
502 r <- forM ts $ \(tid,PerThread{..}) -> do
503 stat <- threadStatus tid
504 let showStat (ThreadBlocked reason) = show reason
505 showStat stat = show stat
506 return [show lbl,show (diffUTCTime tm startTime),showStat stat]
507 hPutClient h $ showColumns r
508#endif
509 ("mem", s) -> cmd0 $ do
510 case s of
511 "gc" -> do hPutClient h "Performing garbage collection..."
512 performMajorGC
513 "" -> do
514 is_enabled <- getGCStatsEnabled
515 if is_enabled
516 then do
517 GCStats{..} <- getGCStats
518 let r = [ ("bytesAllocated", show bytesAllocated)
519 , ("numGcs", show numGcs)
520 , ("maxBytesUsed", show maxBytesUsed)
521 , ("numByteUsageSamples", show numByteUsageSamples)
522 , ("cumulativeBytesUsed", show cumulativeBytesUsed)
523 , ("bytesCopied", show bytesCopied)
524 , ("currentBytesUsed", show currentBytesUsed)
525 , ("currentBytesSlop", show currentBytesSlop)
526 , ("maxBytesSlop", show maxBytesSlop)
527 , ("peakMegabytesAllocated", show peakMegabytesAllocated)
528 , ("mutatorCpuSeconds", show mutatorCpuSeconds)
529 , ("mutatorWallSeconds", show mutatorWallSeconds)
530 , ("gcCpuSeconds", show gcCpuSeconds)
531 , ("gcWallSeconds", show gcWallSeconds)
532 , ("cpuSeconds", show cpuSeconds)
533 , ("wallSeconds", show wallSeconds)
534 , ("parTotBytesCopied", show parTotBytesCopied)
535 , ("parMaxBytesCopied", show parMaxBytesCopied)
536 ]
537 hPutClient h $ showReport r
538 else hPutClient h "Run with +RTS -T to obtain live memory-usage information."
539 _ -> hPutClient h "error."
540
541 ("ls", _) | Just DHT{dhtBuckets} <- Map.lookup netname dhts
542 -> cmd0 $ do
543 bkts <- atomically $ readTVar dhtBuckets
544 let r = reportTable bkts
545 hPutClient h $
546 showReport $
547 r ++ [ ("buckets", show $ R.shape bkts)
548 , ("node-id", show $ thisNode bkts)
549 , ("network", netname) ]
550
551 -- TODO: online documentation.
552 --
553 -- k - manage key-pairs
554 --
555 -- k (list keys)
556 -- k gen (generate new key and list keys)
557 -- k add <secret-key> (input a specific secret key)
558 -- k del <secret-key>
559 -- k secrets (list key pairs, including secret keys)
560
561 ("k", s) | "" <- strp s -> cmd0 $ do
562 ks <- atomically $ readTVar userkeys
563 hPutClient h $ unlines $ map (mappend " " . show . Tox.key2id . snd) ks
564 | "gen" <- strp s -> cmd0 $ do
565 secret <- generateSecretKey
566 let pubkey = toPublic secret
567 oldks <- atomically $ do
568 ks <- readTVar userkeys
569 modifyTVar userkeys ((secret,pubkey):)
570 addRoster roster secret
571 return ks
572 let asString = show . Tox.key2id
573 hPutClient h $ unlines $ map (mappend " " . show . Tox.key2id . snd) oldks
574 ++ [mappend " *" . show . Tox.key2id $ pubkey]
575 | "secrets" <- strp s -> cmd0 $ do
576 ks <- atomically $ readTVar userkeys
577 skey <- maybe (return Nothing) (atomically . dhtSecretKey)
578 $ Map.lookup netname dhts
579 hPutClient h . showReport $ map mkrow ks ++ case skey >>= encodeSecret of
580 Just x -> [("",""),("dht-key:",B.unpack x)]
581 Nothing -> []
582 | ("add":secs) <- words s
583 , mbSecs <- map (decodeSecret . B.pack) secs
584 , all isJust mbSecs -> cmd0 $ do
585 let f (Just b) = b
586 f x = error (concat ["Assertion fail at ", __FILE__, ":", show __LINE__])
587 let toPair x = (x,toPublic x)
588 pairs = map (toPair . f) mbSecs
589 oldks <- atomically $ do
590 oldks <- readTVar userkeys
591 modifyTVar userkeys (pairs ++)
592 forM pairs $ \(sk,_) -> addRoster roster sk
593 return oldks
594 hPutClient h $ unlines $ map (mappend " " . show . Tox.key2id . snd) oldks
595 ++ map (mappend " *" . show . Tox.key2id .snd) pairs
596 | ("del":secs) <- words s
597 , mbSecs <- map (decodeSecret . B.pack) secs
598 , all isJust mbSecs -> cmd0 $ do
599 let f (Just b) = b
600 f x = error (concat ["Assertion fail at ", __FILE__, ":", show __LINE__])
601 let toPair x = (x,toPublic x)
602 pairs = map (toPair . f) mbSecs
603 ks <- atomically $ do
604 modifyTVar userkeys (filter (`notElem` pairs) )
605 forM pairs $ \(_,pk) -> delRoster roster pk
606 readTVar userkeys
607 hPutClient h . showReport $ map mkrow ks
608
609 ("roster", s) -> cmd0 $ join $ atomically $ do
610 dns <- dnsPresentation roster
611 fs <- HashMap.toList <$> friendRequests roster
612 let showFriend (remotekey,fr) =
613 (" " ++ show remotekey, T.unpack $ T.decodeUtf8 $ Tox.friendRequestText fr)
614 showAccount (me,cs) =
615 [(show me,"")] ++ map showFriend cs
616 frs = fs >>= showAccount
617 return $ do
618 hPutClientChunk h $ unlines [ dns, "", "Friend Requests" ]
619 hPutClient h $ showReport frs
620
621 ("onion", s) -> cmd0 $ join $ atomically $ do
622 rm <- readTVar $ routeMap onionRouter
623 ts <- readTVar $ trampolineNodes onionRouter
624 rs <- mapM readTVar (pendingRoutes onionRouter)
625 let showRecord :: Int -> Bool -> [String]
626 showRecord n True = [show n, "pending", ""]
627 showRecord n False
628 | Just RouteRecord{responseCount,timeoutCount} <- IntMap.lookup n rm
629 = [show n, show responseCount, show timeoutCount]
630 | otherwise = [show n, "error!",""]
631 r = map (uncurry showRecord) $ IntMap.toAscList rs
632 return $ do
633 hPutClientChunk h $ "trampolines: " ++ show (IntMap.size ts) ++ "\n"
634 hPutClient h $ showColumns $ ["","responses","timeouts"]:r
635
636
637 ("g", s) | Just DHT{..} <- Map.lookup netname dhts
638 -> cmd0 $ do
639 -- arguments: method
640 -- nid
641 -- (optional dest-ni)
642 self <- atomically $ thisNode <$> readTVar dhtBuckets
643 let (method,xs) = break isSpace $ dropWhile isSpace s
644 (nidstr,ys) = break isSpace $ dropWhile isSpace xs
645 destination = dropWhile isSpace ys
646 goQuery qry = either (hPutClient h . ("Bad search target: "++))
647 (goTarget qry)
648 $ dhtParseId nidstr
649 goTarget DHTQuery{..} nid =
650 go nid >>= reportResult method qshowR qshowTok show h
651 where
652 go | null destination = fmap Right . qhandler self
653 | otherwise = case readEither destination of
654 Right ni -> fmap (maybe (Left "Timeout.") Right)
655 . flip (searchQuery qsearch) ni
656 Left e -> const $ return $ Left ("Bad destination: "++e)
657 maybe (hPutClient h ("Unsupported method: "++method))
658 goQuery
659 $ Map.lookup method dhtQuery
660
661 -- TODO: Online help.
662 --
663 -- p - put/publish a single given datum on a single given node.
664 --
665 -- When destination address (node-addr) is optional, it's absense means to
666 -- publish information in the local node's own database.
667 --
668 -- Bittorrent: (peer) publish yourself as peer in swarm.
669 -- (port) set your current bittorrent listen port.
670 --
671 -- p peer <infohash> <token> [node-addr]
672 --
673 -- p port <num>
674 --
675 -- Tox: (toxid) publish a rendezvous onion route to dht node.
676 -- (friend) send a friend-request over a rendezvous point.
677 -- (dhtkey) send your dht node-id over a rendezvous point.
678 --
679 -- p toxid <key> <token> <node-addr>
680 --
681 -- p friend <key> <nospam> <rendezvous-addr> <text>
682 --
683 -- p dhtkey <key> - <rendezvous-addr>
684
685 ("p", s) | Just DHT{..} <- Map.lookup netname dhts
686 -> cmd0 $ do
687 -- arguments: method
688 -- data
689 -- token
690 -- (optional dest-ni)
691 self <- atomically $ thisNode <$> readTVar dhtBuckets
692 let (method,xs) = break isSpace $ dropWhile isSpace s
693 (dtastr,ys) = break isSpace $ dropWhile isSpace xs
694 (tokenstr,zs) = break isSpace $ dropWhile isSpace ys
695 destination = dropWhile isSpace zs
696 goTarget DHTAnnouncable{..} = do
697 let dta = announceParseData dtastr
698 tok = dta >>= flip announceParseToken tokenstr
699 case liftA2 (,) dta tok of
700 Left e -> hPutClient h e
701 Right nid -> go nid >>= either (hPutClient h) (hPutClient h . show)
702 where
703 go | null destination = fmap (maybe (Left "Timeout.") Right)
704 . flip (uncurry announceSendData) Nothing
705 | otherwise = case announceParseAddress destination of
706 Right ni -> fmap (maybe (Left "Timeout.") Right)
707 . flip (uncurry announceSendData) (Just ni)
708 Left e -> const $ return $ Left ("Bad destination: "++e)
709 maybe (hPutClient h ("Unsupported method: "++method))
710 goTarget
711 $ Map.lookup method dhtAnnouncables
712
713 -- TODO: Online documentation.
714 --
715 -- a - announce, like put/publish but automatically selects nodes to publish on
716 -- and periodically refreshes them.
717 --
718 -- The method name is preceded with a + to start or a - to stop a given
719 -- recurring publication.
720 --
721 -- BitTorrent: (peer) Every minute, announce you are participating
722 -- in a torrent swarm.
723 --
724 -- a +peer <infohash> a -peer <infohash>
725 --
726 -- Tox: (toxid) Every 15 seconds, announce your tox identity to the
727 -- DHT so friends can find you.
728 --
729 -- a +toxid <key>
730 -- a -toxid <key>
731 --
732 -- These probably don't work:
733 -- (experimental) a +friend <nospam> <text>
734 -- (experimental) a +dhtkey
735 ("a", s) | Just DHT{..} <- Map.lookup netname dhts
736 , not (null s)
737 -> cmd0 $ do
738 let (op:method,xs) = break isSpace $ dropWhile isSpace s
739 (dtastr,ys) = break isSpace $ dropWhile isSpace xs
740 a = Map.lookup method dhtAnnouncables
741 q = Map.lookup method dhtQuery
742 doit :: Char -> Announcer -> AnnounceKey -> AnnounceMethod r -> r -> IO ()
743 doit '+' = schedule
744 doit '-' = cancel
745 doit _ = \_ _ _ _ -> hPutClientChunk h "Starting(+) or canceling(-)?"
746 matchingResult ::
747 ( Typeable stok
748 , Typeable ptok
749 , Typeable sni
750 , Typeable pni )
751 => Search nid addr stok sni sr
752 -> (pr -> ptok -> Maybe pni -> IO (Maybe pubr))
753 -> Maybe (stok :~: ptok, sni :~: pni)
754 matchingResult _ _ = liftA2 (,) eqT eqT
755 matchingTok ::
756 ( Typeable stok
757 , Typeable ptok
758 , Typeable sni
759 , Typeable pni )
760 => Search nid addr stok sni sr
761 -> (pr -> ptok -> Maybe pni -> IO (Maybe pubr))
762 -> Maybe (stok :~: ptok)
763 matchingTok _ _ = eqT
764 matchingNI ::
765 ( Typeable stok
766 , Typeable ptok
767 , Typeable sni
768 , Typeable pni )
769 => Search nid addr stok sni sr
770 -> (pr -> ptok -> Maybe pni -> IO (Maybe pubr))
771 -> Maybe (sni :~: pni)
772 matchingNI _ _ = eqT
773 chktok :: Maybe ()
774 chktok = do
775 DHTAnnouncable { announceSendData
776 , announceParseData
777 , announceInterval
778 , qresultAddr } <- a
779 DHTQuery { qsearch } <- q
780 Refl <- matchingTok qsearch announceSendData
781 return ()
782 chkni :: Maybe ()
783 chkni = do
784 DHTAnnouncable { announceSendData
785 , announceParseData
786 , announceInterval
787 , qresultAddr } <- a
788 DHTQuery { qsearch } <- q
789 Refl <- matchingNI qsearch announceSendData
790 return ()
791 mameth = do
792 DHTAnnouncable { announceSendData
793 , announceParseData
794 , announceInterval
795 , qresultAddr } <- a
796 DHTQuery { qsearch } <- q
797 (Refl, Refl) <- matchingResult qsearch announceSendData
798 -- return $ hPutClient h "Type matches."
799 dta <- either (const Nothing) Just $ announceParseData dtastr
800 return $ do
801 akey <- atomically $ packAnnounceKey announcer (method ++ ":" ++ dtastr)
802 doit op announcer
803 akey
804 (AnnounceMethod qsearch announceSendData dhtBuckets
805 (qresultAddr dta)
806 announceInterval)
807 dta
808 case op of
809 '+' -> hPutClient h $ "Announcing at " ++ show (qresultAddr dta) ++ "."
810 '-' -> hPutClient h $ "Canceling " ++ show (qresultAddr dta) ++ "."
811 let aerror = unlines
812 [ "announce error."
813 , "method = " ++ method
814 , "query = " ++ maybe "nil" (const "ok") q
815 , "publish = " ++ maybe "nil" (const "ok") a
816 , "chktok = " ++ maybe "nil" (const "ok") chktok -- chktok = nil
817 , "chkni = " ++ maybe "nil" (const "ok") chkni
818 ]
819 fromMaybe (hPutClient h aerror) mameth
820
821 ("s", s) | Just dht@DHT{..} <- Map.lookup netname dhts
822 -> cmd0 $ do
823 let (method,xs) = break isSpace s
824 (nidstr,ys) = break isSpace $ dropWhile isSpace xs
825 presentSearches = hPutClient h
826 =<< showSearches
827 =<< atomically (readTVar dhtSearches)
828 goTarget qry nid = do
829 kvar <- atomically $ newTVar Nothing
830 -- Forking a thread, but it may ubruptly quit if the following
831 -- STM action decides not to add a new search. This is so that
832 -- I can store the ThreadId into new DHTSearch structure.
833 tid <- fork $ join $ atomically (readTVar kvar >>= maybe retry return)
834 join $ atomically $ do
835 schs <- readTVar dhtSearches
836 case Map.lookup (method,nid) schs of
837 Nothing -> do forkSearch method nid qry dhtSearches dhtBuckets tid kvar
838 return $ presentSearches
839 Just sch -> do writeTVar kvar (Just $ return ())
840 return $ reportSearchResults method h sch
841 goQuery qry = either (hPutClient h . ("Bad search target: "++))
842 (goTarget qry)
843 $ dhtParseId nidstr
844 if null method then presentSearches
845 else maybe (hPutClient h ("Unsupported method: "++method))
846 goQuery
847 $ Map.lookup method dhtQuery
848
849 ("x", s) | Just DHT{..} <- Map.lookup netname dhts
850 -> cmd0 $ do
851 let (method,xs) = break isSpace s
852 (nidstr,ys) = break isSpace $ dropWhile isSpace xs
853 go nid = join $ atomically $ do
854 schs <- readTVar dhtSearches
855 case Map.lookup (method,nid) schs of
856 Nothing -> return $ hPutClient h "No match."
857 Just DHTSearch{searchThread} -> do
858 modifyTVar' dhtSearches (Map.delete (method,nid))
859 return $ do
860 killThread searchThread
861 hPutClient h "Removed search."
862 either (hPutClient h . ("Bad search target: "++)) go $ dhtParseId nidstr
863
864 ("save", _) | Just dht <- Map.lookup netname dhts
865 -> cmd0 $ do
866 saveNodes netname dht
867 hPutClient h $ "Saved " ++ nodesFileName netname ++ "."
868
869 ("load", _) | Just dht <- Map.lookup netname dhts
870 -> cmd0 $ do
871 b <- pingNodes netname dht
872 case b of
873 Just num ->
874 hPutClient h $ unwords [ "Pinging"
875 , show num
876 , "nodes from"
877 , nodesFileName netname ++ "."
878 ]
879 Nothing ->
880 hPutClient h $ "Failed: " ++ nodesFileName netname ++ "."
881
882 ("swarms", s) -> cmd0 $ do
883 let fltr = case s of
884 ('-':'v':cs) | all isSpace (take 1 cs)
885 -> const True
886 _ -> (\(h,c,n) -> c/=0 )
887 ss <- atomically $ Peers.knownSwarms <$> readTVar (Mainline.contactInfo swarms)
888 let r = map (\(h,c,n) -> (unwords [show h,show c], maybe "" show n))
889 $ filter fltr ss
890 hPutClient h $ showReport r
891
892 ("peers", s) -> cmd0 $ case readEither s of
893 Right ih -> do
894 ps <- atomically $ Peers.lookup ih <$> readTVar (Mainline.contactInfo swarms)
895 hPutClient h $ showReport $ map (((,) "") . show . pPrint) ps
896 Left er -> hPutClient h er
897 ("toxids", s) -> cmd0 $ do
898 keydb <- atomically $ readTVar toxkeys
899 now <- getPOSIXTime
900 let entries = map mkentry $ PSQ.toList (Tox.keyByAge keydb)
901 mkentry (k :-> Down tm) = [ show cnt, show k, show (now - tm) ]
902 where Just (_,(cnt,_)) = MM.lookup' k (Tox.keyAssoc keydb)
903 hPutClient h $ showColumns entries
904 ("c", s) | "" <- strp s -> cmd0 $ do
905 let combinedLinkMap = Map.unions $map (dhtLinks . snd) (Map.toList dhts)
906 -- TODO: list all connections
907 let connections = [[{-TODO-}]]
908 hPutClient h $ showColumns connections
909 ("c", s) -> cmd0 $ do
910 let combinedLinkMap = Map.unions $map (dhtLinks . snd) (Map.toList dhts)
911 -- form new connection according of type corresponding to parameter
912 let ws = words s
913 result
914 <- case ws of
915 (linktype:rest)
916 -> case (Map.lookup (head ws) combinedLinkMap) of
917 Nothing -> return . Left $ "I don't know a '" ++ head ws ++ "' link type."
918 Just l@(DHTLink
919 { linkInit {- :: params -> IO (Either String status) -}
920 , linkParamParser {- :: [String] -> Either String params -}
921 , showLinkStatus {- :: status -> String -}
922 }) -> case linkParamParser rest of
923 Left er -> return $ Left er
924 Right params -> fmap showLinkStatus <$> linkInit params
925 _ -> return $ Left "parse error"
926 case result of
927 Left er -> hPutClient h er
928 Right statusstr -> hPutClient h statusstr
929
930 ("help", s) | Just DHT{..} <- Map.lookup netname dhts
931 -> cmd0 $ do
932 let tolist :: a -> [a]
933 tolist = (:[])
934
935 dhtkeys, announcables, links, ks, allcommands :: [[String]]
936 dhtkeys = map tolist $ Map.keys dhts
937 queries = map (tolist . ("s "++)) $ Map.keys dhtQuery
938 xs = map (tolist . ("x "++)) $ Map.keys dhtQuery
939 gs = map (tolist . ("g "++)) $ Map.keys dhtQuery
940 announcables = map (tolist . ("p "++)) $ Map.keys dhtAnnouncables
941 links = map (tolist . ("c "++)) $ Map.keys dhtLinks
942 ks = [["k gen"],["k public"],["k secret"]]
943 allcommands = sortBy (comparing head) $ concat [sessionCommands, dhtkeys, announcables, links, ks, queries, gs,xs]
944
945 hPutClient h ("Available commands:\n" ++ showColumns allcommands)
946
947 _ -> cmd0 $ hPutClient h "error."
948
949
950readExternals :: (ni -> SockAddr) -> [TVar (BucketList ni)] -> IO [SockAddr]
951readExternals nodeAddr vars = do
952 as <- atomically $ mapM (fmap (nodeAddr . selfNode) . readTVar) vars
953 let unspecified (SockAddrInet _ 0) = True
954 unspecified (SockAddrInet6 _ _ (0,0,0,0) _) = True
955 unspecified _ = False
956 -- TODO: Filter to only global addresses?
957 return $ filter (not . unspecified) as
958
959data Options = Options
960 { portbt :: String
961 , porttox :: String
962 , ip6bt :: Bool
963 , ip6tox :: Bool
964 , dhtkey :: Maybe SecretKey
965 }
966 deriving (Eq,Show)
967
968sensibleDefaults :: Options
969sensibleDefaults = Options
970 { portbt = "6881"
971 , porttox = "33445"
972 , ip6bt = True
973 , ip6tox = True
974 , dhtkey = Nothing
975 }
976
977-- bt=<port>,tox=<port>
978-- -4
979parseArgs :: [String] -> Options -> Options
980parseArgs [] opts = opts
981parseArgs ("--dhtkey":k:args) opts = parseArgs args opts
982 { dhtkey = decodeSecret $ B.pack k }
983parseArgs ("-4":args) opts = parseArgs args opts
984 { ip6bt = False
985 , ip6tox = False }
986parseArgs (arg:args) opts = parseArgs args opts
987 { portbt = fromMaybe (portbt opts) $ Prelude.lookup "bt" ports
988 , porttox = fromMaybe (porttox opts) $ Prelude.lookup "tox" ports }
989 where
990 ports = map ( (dropWhile (==',') *** dropWhile (=='='))
991 . break (=='=') )
992 $ groupBy (const (/= ',')) arg
993
994noArgPing :: (x -> IO (Maybe r)) -> [String] -> x -> IO (Maybe r)
995noArgPing f [] x = f x
996noArgPing _ _ _ = return Nothing
997
998main :: IO ()
999main = do
1000 args <- getArgs
1001 let opts = parseArgs args sensibleDefaults
1002 print opts
1003
1004 swarms <- Mainline.newSwarmsDatabase
1005 -- Restore peer database before forking the listener thread.
1006 peerdb <- left show <$> tryIOError (L.readFile "bt-peers.dat")
1007 either (hPutStrLn stderr . ("bt-peers.dat: "++))
1008 (atomically . writeTVar (Mainline.contactInfo swarms))
1009 (peerdb >>= S.decodeLazy)
1010
1011 announcer <- forkAnnouncer
1012
1013 (quitBt,btdhts,btips,baddrs) <- case portbt opts of
1014 "" -> return (return (), Map.empty,return [],[])
1015 p -> do
1016 addr <- getBindAddress p (ip6bt opts)
1017 (bt,btR,btBootstrap4, btBootstrap6) <- Mainline.newClient swarms addr
1018 quitBt <- forkListener "bt" (clientNet bt)
1019 mainlineSearches <- atomically $ newTVar Map.empty
1020 peerPort <- atomically $ newTVar 6881 -- BitTorrent client TCP port.
1021 let mainlineDHT bkts wantip = DHT
1022 { dhtBuckets = bkts btR
1023 , dhtPing = Map.singleton "ping" $ DHTPing
1024 { pingQuery = noArgPing $ fmap (bool Nothing (Just ())) . Mainline.ping bt
1025 , pingShowResult = show
1026 }
1027 , dhtQuery = Map.fromList
1028 [ ("node", DHTQuery
1029 { qsearch = (Mainline.nodeSearch bt)
1030 , qhandler = (\ni -> fmap Mainline.unwrapNodes
1031 . Mainline.findNodeH btR ni
1032 . flip Mainline.FindNode (Just Want_Both))
1033 , qshowR = show
1034 , qshowTok = (const Nothing)
1035 })
1036 -- Maybe (sr :~: pr, stok :~: ptok, sni :~: pni )
1037 -- sr = InfoHash
1038 -- stok = Token
1039 -- sni = NodeInfo
1040 , ("peer", DHTQuery
1041 { qsearch = (Mainline.peerSearch bt)
1042 , qhandler = (\ni -> fmap Mainline.unwrapPeers
1043 . Mainline.getPeersH btR swarms ni
1044 . flip Mainline.GetPeers (Just Want_Both)
1045 . (read . show)) -- TODO: InfoHash -> NodeId
1046 , qshowR = (show . pPrint)
1047 , qshowTok = (Just . show)
1048 })
1049 ]
1050 , dhtParseId = readEither :: String -> Either String Mainline.NodeId
1051 , dhtSearches = mainlineSearches
1052 , dhtFallbackNodes = Mainline.bootstrapNodes wantip
1053 , dhtAnnouncables = Map.fromList
1054 -- Maybe (sr :~: pr, stok :~: ptok, sni :~: pni )
1055 -- dta = Announce
1056 -- pr = Announced
1057 -- ptok = Token
1058 -- pni = NodeInfo
1059 [ ("peer", DHTAnnouncable { announceSendData = \ih tok -> \case
1060 Just ni -> do
1061 port <- atomically $ readTVar peerPort
1062 let dta = Mainline.mkAnnounce port ih tok
1063 Mainline.announce bt dta ni
1064 Nothing -> return Nothing
1065 , announceParseAddress = readEither
1066 , announceParseData = readEither
1067 , announceParseToken = const $ readEither
1068 , announceInterval = 60 -- TODO: Is one minute good?
1069 , qresultAddr = (read . show) -- TODO: InfoHash -> NodeId -- peer
1070 })
1071 , ("port", DHTAnnouncable { announceParseData = readEither
1072 , announceParseToken = \_ _ -> return ()
1073 , announceParseAddress = const $ Right ()
1074 , announceSendData = \dta () -> \case
1075 Nothing -> do atomically $ writeTVar peerPort (dta :: PortNumber)
1076 return $ Just dta
1077 Just _ -> return Nothing
1078 , announceInterval = 0 -- TODO: The "port" setting should probably
1079 -- be a command rather than an announcement.
1080 , qresultAddr = const $ Mainline.zeroID
1081 })]
1082
1083 , dhtLinks = Map.fromList
1084 [ {- TODO -}
1085 ]
1086 , dhtSecretKey = return Nothing
1087 , dhtBootstrap = case wantip of
1088 Want_IP4 -> btBootstrap4
1089 Want_IP6 -> btBootstrap6
1090 }
1091 dhts = Map.fromList $
1092 ("bt4", mainlineDHT Mainline.routing4 Want_IP4)
1093 : if ip6bt opts
1094 then [ ("bt6", mainlineDHT Mainline.routing6 Want_IP6) ]
1095 else []
1096 ips :: IO [SockAddr]
1097 ips = readExternals Mainline.nodeAddr
1098 [ Mainline.routing4 btR
1099 , Mainline.routing6 btR
1100 ]
1101 return (quitBt,dhts,ips, [addr])
1102
1103 keysdb <- Tox.newKeysDatabase
1104
1105 crypto <- Tox.newCrypto
1106 netCryptoSessionsState <- Tox.newSessionsState crypto Tox.defaultUnRecHook Tox.defaultCryptoDataHooks
1107
1108 (mbtox,quitTox,toxdhts,toxips,taddrs) <- case porttox opts of
1109 "" -> return (Nothing,return (), Map.empty, return [],[])
1110 toxport -> do
1111 addrTox <- getBindAddress toxport (ip6tox opts)
1112 hPutStrLn stderr $ "Supplied key: " ++ show (fmap (Tox.key2id . toPublic) (dhtkey opts))
1113 tox <- Tox.newTox keysdb addrTox (Just netCryptoSessionsState) (dhtkey opts)
1114 (quitTox, toxStrap4, toxStrap6) <- Tox.forkTox tox
1115
1116 toxSearches <- atomically $ newTVar Map.empty
1117
1118 let toxDHT bkts wantip = DHT
1119 { dhtBuckets = bkts (Tox.toxRouting tox)
1120 , dhtPing = Map.fromList
1121 [ ("ping", DHTPing
1122 { pingQuery = noArgPing $ fmap (bool Nothing (Just ())) . Tox.ping (Tox.toxDHT tox)
1123 , pingShowResult = show
1124 })
1125 , ("cookie", DHTPing
1126 { pingQuery = \case
1127 [keystr] | Just mykey <- readMaybe keystr
1128 -> Tox.cookieRequest (Tox.toxCryptoKeys tox)
1129 (Tox.toxDHT tox)
1130 (Tox.id2key mykey)
1131 _ -> const $ return Nothing
1132 , pingShowResult = show
1133 })]
1134 , dhtQuery = Map.fromList
1135 [ ("node", DHTQuery
1136 { qsearch = (Tox.nodeSearch $ Tox.toxDHT tox)
1137 , qhandler = (\ni -> fmap Tox.unwrapNodes
1138 . Tox.getNodesH (Tox.toxRouting tox) ni
1139 . Tox.GetNodes)
1140 , qshowR = show -- NodeInfo
1141 , qshowTok = (const Nothing)
1142 })
1143 , ("toxid", DHTQuery
1144 { qsearch = (Tox.toxidSearch (Tox.onionTimeout tox)
1145 (Tox.toxCryptoKeys tox)
1146 (Tox.toxOnion tox))
1147 , qhandler = -- qhandler :: ni -> nid -> IO ([ni], [r], tok)
1148 (\ni nid ->
1149 Tox.unwrapAnnounceResponse Nothing
1150 <$> clientAddress (Tox.toxDHT tox) Nothing
1151 <*> Tox.announceH (Tox.toxRouting tox)
1152 (Tox.toxTokens tox)
1153 (Tox.toxAnnouncedKeys tox)
1154 (Tox.OnionDestination Tox.SearchingAlias ni Nothing)
1155 (Tox.AnnounceRequest zeros32 nid Tox.zeroID))
1156 , qshowR = show -- Rendezvous
1157 , qshowTok = Just . show -- Nonce32
1158 })
1159 ]
1160 , dhtParseId = readEither :: String -> Either String Tox.NodeId
1161 , dhtSearches = toxSearches
1162 , dhtFallbackNodes = return []
1163 , dhtAnnouncables = Map.fromList
1164 [ ("toxid", DHTAnnouncable { announceSendData = \pubkey token -> \case
1165 Just ni ->
1166 Tox.putRendezvous
1167 (Tox.onionTimeout tox)
1168 (Tox.toxCryptoKeys tox)
1169 (Tox.toxOnion tox)
1170 (pubkey :: PublicKey)
1171 (token :: Nonce32)
1172 ni
1173 Nothing -> return Nothing
1174 , announceParseAddress = readEither
1175 , announceParseToken = const $ readEither
1176 , announceParseData = fmap Tox.id2key . readEither
1177 , qresultAddr = Tox.key2id -- toxid
1178
1179 -- For peers we are announcing ourselves to, if we are not
1180 -- announced to them toxcore tries every 3 seconds to
1181 -- announce ourselves to them until they return that we
1182 -- have announced ourselves to, then toxcore sends an
1183 -- announce request packet every 15 seconds to see if we
1184 -- are still announced and re announce ourselves at the
1185 -- same time. The timeout of 15 seconds means a `ping_id`
1186 -- received in the last packet will not have had time to
1187 -- expire (20 second minimum timeout) before it is resent
1188 -- 15 seconds later. Toxcore sends every announce packet
1189 -- with the `ping_id` previously received from that peer
1190 -- with the same path (if possible).
1191 , announceInterval = 15
1192
1193 })
1194 , ("dhtkey", DHTAnnouncable { announceSendData = \pubkey () -> \case
1195 Just addr -> do
1196 dkey <- Tox.getContactInfo tox
1197 sendMessage
1198 (Tox.toxToRoute tox)
1199 (addr :: Tox.AnnouncedRendezvous)
1200 (pubkey,Tox.OnionDHTPublicKey dkey)
1201 return $ Just ()
1202 Nothing -> return Nothing
1203 , announceParseAddress = readEither
1204 , announceParseToken = \_ _ -> return ()
1205 , announceParseData = fmap Tox.id2key . readEither
1206 , qresultAddr = Tox.key2id
1207
1208 -- We send this packet every 30 seconds if there is more
1209 -- than one peer (in the 8) that says they our friend is
1210 -- announced on them. This packet can also be sent through
1211 -- the DHT module as a DHT request packet (see DHT) if we
1212 -- know the DHT public key of the friend and are looking
1213 -- for them in the DHT but have not connected to them yet.
1214 -- 30 second is a reasonable timeout to not flood the
1215 -- network with too many packets while making sure the
1216 -- other will eventually receive the packet. Since packets
1217 -- are sent through every peer that knows the friend,
1218 -- resending it right away without waiting has a high
1219 -- likelihood of failure as the chances of packet loss
1220 -- happening to all (up to to 8) packets sent is low.
1221 --
1222 , announceInterval = 30
1223
1224 })
1225 , ("friend", DHTAnnouncable { announceSendData = \pubkey nospam -> \case
1226 Just addr -> do
1227 let fr = Tox.FriendRequest nospam txt
1228 -- nospam = 0xD64A8B00
1229 txt = "Testing Friend Request!"
1230 sendMessage
1231 (Tox.toxToRoute tox)
1232 (addr :: Tox.AnnouncedRendezvous)
1233 (pubkey,Tox.OnionFriendRequest fr)
1234 return $ Just ()
1235 Nothing -> return Nothing
1236 , announceParseAddress = readEither
1237 , announceParseData = fmap Tox.id2key . readEither
1238 , announceParseToken = \pubkey nospamstr -> do
1239 Tox.NoSpam nospam chksum <- readEither nospamstr
1240 maybe (Right ())
1241 (Tox.verifyChecksum pubkey)
1242 chksum
1243 return nospam
1244 , qresultAddr = Tox.key2id
1245
1246 -- Friend requests are sent with exponentially increasing
1247 -- interval of 2 seconds, 4 seconds, 8 seconds, etc... in
1248 -- toxcore. This is so friend requests get resent but
1249 -- eventually get resent in intervals that are so big that
1250 -- they essentially expire. The sender has no way of
1251 -- knowing if a peer refuses a friend requests which is why
1252 -- friend requests need to expire in some way. Note that
1253 -- the interval is the minimum timeout, if toxcore cannot
1254 -- send that friend request it will try again until it
1255 -- manages to send it. One reason for not being able to
1256 -- send the friend request would be that the onion has not
1257 -- found the friend in the onion and so cannot send an
1258 -- onion data packet to them.
1259 --
1260 -- TODO: Support exponential backoff behavior. For now, setting
1261 -- interval to 8 seconds.
1262
1263 , announceInterval = 8
1264 })]
1265 , dhtLinks = Map.fromList
1266 [ {- TODO -}
1267 ]
1268 , dhtSecretKey = return $ Just $ transportSecret (Tox.toxCryptoKeys tox)
1269 , dhtBootstrap = case wantip of
1270 Want_IP4 -> toxStrap4
1271 Want_IP6 -> toxStrap6
1272 }
1273 dhts = Map.fromList $
1274 ("tox4", toxDHT Tox.routing4 Want_IP4)
1275 : if ip6tox opts
1276 then [ ("tox6", toxDHT Tox.routing6 Want_IP6) ]
1277 else []
1278 ips :: IO [SockAddr]
1279 ips = readExternals Tox.nodeAddr [ Tox.routing4 $ Tox.toxRouting tox
1280 , Tox.routing6 $ Tox.toxRouting tox ]
1281 return (Just tox, quitTox, dhts, ips, [addrTox])
1282 _ <- UPNP.requestPorts "dht-client" $ map (Datagram,) $ baddrs ++ taddrs
1283
1284 let dhts = Map.union btdhts toxdhts
1285
1286 waitForSignal <- do
1287 signalQuit <- newEmptyMVar
1288 let defaultToxData = do
1289 toxids <- atomically $ newTVar []
1290 rster <- newRoster
1291 orouter <- newOnionRouter (hPutStrLn stderr)
1292 return (toxids, rster, orouter)
1293 (toxids,rstr,orouter) <- fromMaybe defaultToxData $ do
1294 tox <- mbtox
1295 return $ return ( userKeys (Tox.toxCryptoKeys tox), Tox.toxRoster tox, Tox.toxOnionRoutes tox )
1296 let session = clientSession0 $ Session
1297 { netname = concat $ take 1 $ Map.keys dhts -- initial default DHT
1298 , dhts = dhts -- all DHTs
1299 , signalQuit = signalQuit
1300 , swarms = swarms
1301 , cryptosessions = netCryptoSessionsState
1302 , toxkeys = keysdb
1303 , userkeys = toxids
1304 , roster = rstr
1305 , onionRouter = orouter
1306 , externalAddresses = liftM2 (++) btips toxips
1307 , announcer = announcer
1308 }
1309 srv <- streamServer (withSession session) (SockAddrUnix "dht.sock")
1310 return $ do
1311 () <- takeMVar signalQuit
1312 quitListening srv
1313
1314
1315 forM_ (Map.toList dhts)
1316 $ \(netname, dht@DHT { dhtBuckets = bkts
1317 , dhtQuery = qrys
1318 , dhtPing = pings
1319 , dhtFallbackNodes = getBootstrapNodes
1320 , dhtBootstrap = bootstrap }) -> do
1321 btSaved <- loadNodes netname -- :: IO [Mainline.NodeInfo]
1322 putStrLn $ "Loaded "++show (length btSaved)++" nodes for "++netname++"."
1323 fallbackNodes <- getBootstrapNodes
1324 let isNodesSearch :: ni :~: r -> Search nid addr tok ni r -> Search nid addr tok ni ni
1325 isNodesSearch Refl sch = sch
1326 ping = maybe (const $ return False)
1327 (\DHTPing{pingQuery} -> fmap (maybe False (const True)) . pingQuery [])
1328 $ Map.lookup "ping" pings
1329 fork $ do
1330 myThreadId >>= flip labelThread ("bootstrap."++netname)
1331 bootstrap btSaved fallbackNodes
1332 return ()
1333
1334 waitForSignal
1335
1336 stopAnnouncer announcer
1337 quitBt
1338 quitTox
1339
1340 swarmsdb <- atomically $ readTVar (Mainline.contactInfo swarms)
1341 L.writeFile "bt-peers.dat" $ S.encodeLazy swarmsdb
diff --git a/readpackets.hs b/readpackets.hs
new file mode 100644
index 00000000..f02df538
--- /dev/null
+++ b/readpackets.hs
@@ -0,0 +1,82 @@
1{-# LANGUAGE PackageImports #-}
2module Main where
3
4import Data.Binary.Get (runGet)
5import qualified Data.ByteString as BS
6import qualified Data.ByteString as B
7import qualified Data.ByteString.Lazy as LZ
8import qualified Data.ByteString.Lazy.Char8 as L8
9import Data.IORef
10import Data.List
11import Debug.Trace
12import Text.Printf
13import Text.Show.Pretty as PP
14import "network-house" Net.Packet
15import qualified "network-house" Net.IPv4 as IP4
16import qualified "network-house" Net.IPv6 as IP6
17import "network-house" Net.PacketParsing
18import "network-house" Net.UDP as UDP
19import "pcap" Network.Pcap
20import qualified Data.Serialize as S
21import qualified Network.Socket as HS
22import Control.Applicative
23
24import Crypto.Tox
25import Network.Tox.DHT.Transport as Tox
26import Data.BEncode as BE
27import Data.BEncode.Pretty
28-- import Data.IKE.Message
29
30-- traceM string = trace string $ return ()
31
32bs2chunk :: BS.ByteString -> UArray Int Word8
33bs2chunk bs = listArray (0,subtract 33 $ BS.length bs) $ drop 32 $ BS.unpack bs
34
35hex :: BS.ByteString -> String
36hex = concatMap (printf "%02x") . B.unpack
37
38hexlines :: BS.ByteString -> [String]
39hexlines bs = ss
40 where xs = zip [0..] $ hex bs
41 ls = groupBy (\(n,_) (m,_)-> n `div` 32 == m `div` 32) xs
42 ss = map (map snd) ls
43
44parsePacket :: IORef Int -> PktHdr -> BS.ByteString -> IO ()
45parsePacket cnt hdr buf = do
46 print hdr
47 let -- mb :: Maybe (BS.ByteString, Message Encrypted)
48 mb = do
49 udp <- doParse $ toInPack $ bs2chunk buf
50 -- traceM $ "got udp: " ++ show udp
51 -- traceM $ "got udp content: " ++ show (content udp)
52 let plen = Net.Packet.len $ content udp
53 bs <- fmap BS.pack . parseInPacket (bytes plen) $ content udp
54 -- traceM $ "Got bs " ++ show bs
55 let (checksum,blob) = BS.splitAt 2 bs -- extra 2 bytes in pcap capture, i'm assuming its a checksum
56-- (first4,truncated) = BS.splitAt 4 blob
57-- -- First 4 bytes being zero is how we distinguish between
58-- -- ESP and IKEv2 packets on port 4500.
59-- dta = if destPort udp /= Port 500 && first4==BS.pack [0,0,0,0]
60-- then truncated
61-- else blob
62 dta = blob
63 let d = BE.decode {- runGet getMessage $ LZ.fromStrict -} dta
64 saddr = HS.SockAddrInet 0 0 -- TODO
65 -- e = S.decode dta :: Either String (DHTMessage Encrypted8)
66 e = case Tox.parseDHTAddr (dta,saddr) :: Either (DHTMessage Encrypted8,NodeInfo) (BS.ByteString,HS.SockAddr) of
67 Left toxpkt -> Right toxpkt
68 Right _ -> Left "tox parse fail"
69 return $ (udp, bs, fmap Left d <|> fmap Right e)
70 flip (maybe $ return ()) mb $ \(udp, bs,m) -> do
71 putStrLn $ show udp
72 mapM_ putStrLn $ hexlines bs
73 -- putStrLn $ PP.ppShow m
74 either putStrLn (L8.putStrLn . either showBEncode (L8.pack . show)) m
75 modifyIORef' cnt (+1)
76
77main = do
78 cnt <- newIORef 0
79 pcap <- openOffline "packets.pcap"
80 loopResult <- loopBS pcap (-1) $ parsePacket cnt
81 pktcnt <- readIORef cnt
82 putStrLn $ "read "++show pktcnt ++" packets."
diff --git a/src/Control/Concurrent/Async/Lifted/Instrument.hs b/src/Control/Concurrent/Async/Lifted/Instrument.hs
new file mode 100644
index 00000000..eab0fadc
--- /dev/null
+++ b/src/Control/Concurrent/Async/Lifted/Instrument.hs
@@ -0,0 +1,5 @@
1module Control.Concurrent.Async.Lifted.Instrument
2 ( module Control.Concurrent.Async.Lifted
3 ) where
4
5import Control.Concurrent.Async.Lifted
diff --git a/src/Control/Concurrent/Lifted/Instrument.hs b/src/Control/Concurrent/Lifted/Instrument.hs
new file mode 100644
index 00000000..7e4a7356
--- /dev/null
+++ b/src/Control/Concurrent/Lifted/Instrument.hs
@@ -0,0 +1,88 @@
1{-# LANGUAGE FlexibleContexts #-}
2module Control.Concurrent.Lifted.Instrument
3 ( module Control.Concurrent.Lifted
4 , forkIO
5 , fork
6 , labelThread
7 , threadsInformation
8 , PerThread(..)
9 ) where
10
11import qualified Control.Concurrent.Lifted as Raw
12import Control.Concurrent.Lifted hiding (fork)
13import Control.Exception (fromException)
14import Control.Monad.Trans.Control
15import System.IO.Unsafe
16import qualified Data.Map.Strict as Map
17import Control.Exception.Lifted
18import Control.Monad.Base
19import qualified GHC.Conc as GHC
20import Data.Time()
21import Data.Time.Clock
22import System.IO
23import Control.Monad.IO.Class
24
25
26data PerThread = PerThread
27 { lbl :: String
28 , startTime :: UTCTime
29 }
30 deriving (Eq,Ord,Show)
31
32data GlobalState = GlobalState
33 { threads :: !(Map.Map ThreadId PerThread)
34 , reportException :: String -> IO ()
35 }
36
37globals :: MVar GlobalState
38globals = unsafePerformIO $ newMVar $ GlobalState
39 { threads = Map.empty
40 , reportException = hPutStrLn stderr
41 }
42{-# NOINLINE globals #-}
43
44
45forkIO :: IO () -> IO ThreadId
46forkIO = fork
47{-# INLINE forkIO #-}
48
49fork :: (MonadBaseControl IO m, MonadIO m) => m () -> m ThreadId
50fork action = do
51 t <- Raw.fork $ do
52 tid <- myThreadId
53 tm <- liftBase getCurrentTime
54 bracket_ (modifyThreads $! Map.insert tid (PerThread "" tm))
55 (return ())
56 $ do catch action $ \e -> case fromException e of
57 Just ThreadKilled -> return ()
58 Nothing -> liftIO $ do
59 g <- takeMVar globals
60 let l = concat [ show e
61 , " ("
62 , maybe "" lbl $ Map.lookup tid (threads g)
63 , ")"
64 ]
65 reportException g l
66 putMVar globals $! g { threads = Map.insert tid (PerThread l tm) $ threads g }
67 throwIO e
68 -- Remove the thread only if it terminated normally or was killed.
69 modifyThreads $! Map.delete tid
70 return t
71
72labelThread :: ThreadId -> String -> IO ()
73labelThread tid s = do
74 GHC.labelThread tid s
75 modifyThreads $! Map.adjust (\pt -> pt { lbl = s }) tid
76{-# INLINE labelThread #-}
77
78threadsInformation :: IO [(ThreadId,PerThread)]
79threadsInformation = do
80 m <- threads <$> readMVar globals
81 return $ Map.toList m
82
83
84modifyThreads :: MonadBase IO m => (Map.Map ThreadId PerThread -> Map.Map ThreadId PerThread) -> m ()
85modifyThreads f = do
86 g <- takeMVar globals
87 let f' st = st { threads = f (threads st) }
88 putMVar globals $! f' g
diff --git a/src/Control/Concurrent/Tasks.hs b/src/Control/Concurrent/Tasks.hs
new file mode 100644
index 00000000..287542ee
--- /dev/null
+++ b/src/Control/Concurrent/Tasks.hs
@@ -0,0 +1,44 @@
1{-# LANGUAGE CPP #-}
2module Control.Concurrent.Tasks where
3
4import Control.Concurrent.STM
5import Control.Exception
6import Data.Function
7import Data.List
8#ifdef THREAD_DEBUG
9import Control.Concurrent.Lifted.Instrument
10#else
11import Control.Concurrent.Lifted
12import GHC.Conc (labelThread)
13#endif
14
15newtype TaskGroup = TaskGroup
16 { taskQueue :: TChan (String,IO ())
17 }
18
19withTaskGroup :: String -> Int -> (TaskGroup -> IO ()) -> IO ()
20withTaskGroup glabel numslots action = do
21 tg <- atomically $ newTChan
22 cnt <- atomically $ newTVar 0
23 thread <- fork $ do
24 myThreadId >>= flip labelThread glabel
25 fix $ \again -> do
26 (slot, (lbl,task)) <- atomically $ do
27 slot <- readTVar cnt
28 check (slot < numslots)
29 writeTVar cnt (succ slot)
30 t <- readTChan tg
31 return (slot,t)
32 _ <- fork $ do
33 myThreadId >>= flip labelThread (intercalate "." [glabel,show slot,lbl])
34 task `catch` (\(SomeException e) -> return ())
35 atomically $ modifyTVar' cnt pred
36 again
37 action (TaskGroup tg) `onException` killThread thread
38 atomically $ do
39 isEmptyTChan tg >>= check
40 readTVar cnt >>= check . (== 0)
41 killThread thread
42
43forkTask :: TaskGroup -> String -> IO () -> IO ()
44forkTask (TaskGroup q) lbl action = atomically $ writeTChan q (lbl,action)
diff --git a/src/Control/TriadCommittee.hs b/src/Control/TriadCommittee.hs
new file mode 100644
index 00000000..88e665b6
--- /dev/null
+++ b/src/Control/TriadCommittee.hs
@@ -0,0 +1,89 @@
1{-# LANGUAGE TupleSections #-}
2module Control.TriadCommittee where
3
4import Control.Concurrent.STM
5import Control.Monad
6import Data.Maybe
7
8
9data TriadSlot = SlotA | SlotB | SlotC
10 deriving (Eq,Ord,Enum,Show,Read)
11
12data TriadCommittee voter a = TriadCommittee
13 { triadDecider :: TVar TriadSlot
14 , triadA :: TVar (Maybe (voter,a))
15 , triadB :: TVar (Maybe (voter,a))
16 , triadC :: TVar (Maybe (voter,a))
17 , triadNewDecision :: a -> STM ()
18 }
19
20triadSlot :: TriadSlot -> TriadCommittee voter a -> TVar (Maybe (voter,a))
21triadSlot SlotA = triadA
22triadSlot SlotB = triadB
23triadSlot SlotC = triadC
24
25triadDecision :: a -> TriadCommittee voter a -> STM a
26triadDecision fallback triad = do
27 slot <- readTVar (triadDecider triad)
28 maybe fallback snd <$> readTVar (triadSlot slot triad)
29
30
31newTriadCommittee :: (a -> STM ()) -> STM (TriadCommittee voter a)
32newTriadCommittee onChange =
33 TriadCommittee <$> newTVar SlotA
34 <*> newTVar Nothing
35 <*> newTVar Nothing
36 <*> newTVar Nothing
37 <*> pure onChange
38
39
40triadCountVotes :: Eq a => Maybe a -> TriadCommittee voter a -> STM ()
41triadCountVotes prior triad = do
42 a <- fmap ((SlotA,) . snd) <$> readTVar (triadA triad)
43 b <- fmap ((SlotB,) . snd) <$> readTVar (triadB triad)
44 c <- fmap ((SlotC,) . snd) <$> readTVar (triadC triad)
45 let (slot,vote) = case catMaybes [a,b,c] of
46 [ (x,xvote)
47 , (y,yvote)
48 , (z,zvote) ] -> if xvote == yvote then (x,Just xvote)
49 else (z,Just zvote)
50 [] -> (SlotA,Nothing)
51 ((slot,vote):_) -> (slot, Just vote)
52 writeTVar (triadDecider triad) slot
53 case vote of
54 Just v | vote /= prior -> triadNewDecision triad v
55 _ -> return ()
56
57
58addVote :: (Eq voter, Eq a) => TriadCommittee voter a -> voter -> a -> STM ()
59addVote triad voter vote = do
60 a <- (SlotA,) . fmap fst <$> readTVar (triadA triad)
61 b <- (SlotB,) . fmap fst <$> readTVar (triadB triad)
62 c <- (SlotC,) . fmap fst <$> readTVar (triadC triad)
63 let avail (_,Nothing) = True
64 avail (_,Just x ) = (x == voter)
65 slots = filter avail [a,b,c]
66 forM_ (take 1 slots) $ \(slot,_) -> do
67 prior <- do
68 slotp <- readTVar (triadDecider triad)
69 fmap snd <$> readTVar (triadSlot slotp triad)
70 writeTVar (triadSlot slot triad)
71 (Just (voter,vote))
72 triadCountVotes prior triad
73
74
75delVote :: (Eq voter, Eq a) => TriadCommittee voter a -> voter -> STM ()
76delVote triad voter = do
77 a <- (SlotA,) . fmap fst <$> readTVar (triadA triad)
78 b <- (SlotB,) . fmap fst <$> readTVar (triadB triad)
79 c <- (SlotC,) . fmap fst <$> readTVar (triadC triad)
80 let match (_,Just x ) = (x == voter)
81 match _ = False
82 slots = filter match [a,b,c]
83 forM_ (take 1 slots) $ \(slot,_) -> do
84 prior <- do
85 slotp <- readTVar (triadDecider triad)
86 fmap snd <$> readTVar (triadSlot slotp triad)
87 writeTVar (triadSlot slot triad) Nothing
88 triadCountVotes prior triad
89
diff --git a/src/Crypto/Tox.hs b/src/Crypto/Tox.hs
new file mode 100644
index 00000000..665a38dd
--- /dev/null
+++ b/src/Crypto/Tox.hs
@@ -0,0 +1,571 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3{-# LANGUAGE ScopedTypeVariables #-}
4{-# LANGUAGE KindSignatures #-}
5{-# LANGUAGE DeriveDataTypeable #-}
6{-# LANGUAGE DeriveFunctor #-}
7{-# LANGUAGE DeriveTraversable #-}
8{-# LANGUAGE ExplicitNamespaces #-}
9{-# LANGUAGE TypeOperators #-}
10{-# LANGUAGE MagicHash, UnboxedTuples, BangPatterns #-}
11{-# LANGUAGE NamedFieldPuns #-}
12module Crypto.Tox
13 ( PublicKey
14 , publicKey
15 , getPublicKey
16 , putPublicKey
17 , SecretKey
18 , generateSecretKey
19 , toPublic
20 , SymmetricKey(..)
21 , TransportCrypto(..)
22 , SecretsCache
23 , newSecretsCache
24 , Encrypted
25 , Encrypted8(..)
26 , type (∘)(..)
27 , Asymm(..)
28 , getAsymm
29 , getAliasedAsymm
30 , putAsymm
31 , putAliasedAsymm
32 , Plain
33 , encodePlain
34 , decodePlain
35 -- , computeSharedSecret
36 , lookupSharedSecret
37 , encrypt
38 , decrypt
39 , Nonce8(..)
40 , Nonce24(..)
41 , incrementNonce24
42 , addtoNonce24
43 , Nonce32(..)
44 , getRemainingEncrypted
45 , putEncrypted
46 , Auth
47 , Sized(..)
48 , Size(..)
49 , State(..)
50 , zeros32
51 , zeros24
52 , decryptSymmetric
53 , encryptSymmetric
54 , encodeSecret
55 , decodeSecret
56 ) where
57
58import Control.Arrow
59import Control.Monad
60import qualified Crypto.Cipher.ChaChaPoly1305 as Symmetric
61import qualified Crypto.Cipher.Salsa as Salsa
62import qualified Crypto.Cipher.XSalsa as XSalsa
63import Crypto.ECC.Class
64import qualified Crypto.Error as Cryptonite
65import qualified Crypto.MAC.Poly1305 as Poly1305
66import Crypto.PubKey.Curve25519
67import Data.Bits
68import qualified Data.ByteArray as BA
69 ;import Data.ByteArray as BA (ByteArrayAccess, Bytes)
70import Data.ByteString as B
71import qualified Data.ByteString.Base16 as Base16
72import qualified Data.ByteString.Base64 as Base64
73import qualified Data.ByteString.Char8 as C8
74import Data.Data
75import Data.Functor.Contravariant
76#if MIN_VERSION_base(4,9,1)
77import Data.Kind
78#else
79import GHC.Exts (Constraint)
80#endif
81import Data.Ord
82import Data.Serialize as S
83import Data.Word
84import Foreign.Marshal.Alloc
85import Foreign.Ptr
86import Foreign.Storable
87import System.Endian
88import qualified Data.ByteString.Internal
89import Control.Concurrent.STM
90import Crypto.Error.Types (CryptoFailable (..), throwCryptoError)
91import Network.Socket (SockAddr)
92import GHC.Exts (Word(..),inline)
93import GHC.Prim
94import Data.Word64Map (fitsInInt)
95import Data.MinMaxPSQ (MinMaxPSQ')
96import qualified Data.MinMaxPSQ as MM
97import Data.Time.Clock.POSIX
98import Data.Hashable
99import System.IO.Unsafe (unsafeDupablePerformIO)
100
101-- | A 16-byte mac and an arbitrary-length encrypted stream.
102newtype Encrypted a = Encrypted ByteString
103 deriving (Eq,Ord,Data,ByteArrayAccess)
104
105newtype Encrypted8 a = E8 (Encrypted (a,Nonce8))
106 deriving (Serialize, Show)
107
108newtype (f ∘ g) x = Composed { uncomposed :: f (g x) }
109
110infixr ∘
111
112newtype Auth = Auth Poly1305.Auth deriving (Eq, ByteArrayAccess)
113instance Ord Auth where
114 compare (Auth a) (Auth b) = comparing (BA.convert :: Poly1305.Auth -> Bytes) a b
115instance Data Auth where
116 gfoldl k z x = z x
117 -- Well, this is a little wonky... XXX
118 gunfold k z c = k (z (Auth . Poly1305.Auth . (BA.convert :: ByteString -> Bytes)))
119 toConstr _ = con_Auth
120 dataTypeOf _ = mkDataType "Crypto.Tox" [con_Auth]
121con_Auth :: Constr
122con_Auth = mkConstr (dataTypeOf (Auth (error "con_Auth"))) "Auth" [] Prefix
123instance Serialize Auth where
124 get = Auth . Poly1305.Auth . BA.convert <$> getBytes 16
125 put (Auth (Poly1305.Auth bs)) = putByteString $ BA.convert bs
126
127instance Typeable a => Show (Encrypted a) where
128 show (Encrypted _) = "Encrypted "++show (typeOf (undefined :: a))
129
130encryptedAuth :: Encrypted a -> Auth
131encryptedAuth (Encrypted bs)
132 | Right auth <- decode (B.take 16 bs) = auth
133 | otherwise = error "encryptedAuth: insufficient bytes"
134
135authAndBytes :: Encrypted a -> (Auth, ByteString)
136authAndBytes (Encrypted bs) = (auth,bs')
137 where
138 (as,bs') = B.splitAt 16 bs
139 Right auth = decode as
140
141-- | Info about a type's serialized length. Either the length is known
142-- independently of the value, or the length depends on the value.
143data Size a
144 = VarSize (a -> Int)
145 | ConstSize !Int
146 deriving Typeable
147
148instance Contravariant Size where
149 contramap f sz = case sz of
150 ConstSize n -> ConstSize n
151 VarSize g -> VarSize (\x -> g (f x))
152
153instance Monoid (Size a) where
154 ConstSize x `mappend` ConstSize y = ConstSize (x + y)
155 VarSize f `mappend` ConstSize y = VarSize $ \x -> f x + y
156 ConstSize x `mappend` VarSize g = VarSize $ \y -> x + g y
157 VarSize f `mappend` VarSize g = VarSize $ \x -> f x + g x
158 mempty = ConstSize 0
159
160
161class Sized a where size :: Size a
162
163instance Sized a => Serialize (Encrypted a) where
164 get = case size :: Size a of
165 VarSize _ -> Encrypted <$> (remaining >>= getBytes)
166 ConstSize n -> Encrypted <$> getBytes (16 + n) -- 16 extra for Poly1305 mac
167 put = putEncrypted
168
169instance Sized a => Sized (Encrypted a) where
170 size = case size :: Size a of
171 ConstSize n -> ConstSize $ n + 16
172 VarSize _ -> VarSize $ \(Encrypted bs) -> B.length bs
173
174instance (Sized a, Sized b) => Sized (a,b) where
175 size = case (size :: Size a, size :: Size b) of
176 (ConstSize a , ConstSize b) -> ConstSize $ a + b
177 (VarSize f , ConstSize b) -> VarSize $ \(a, _) -> f a + b
178 (ConstSize a , VarSize g) -> VarSize $ \(_, b) -> a + g b
179 (VarSize f , VarSize g) -> VarSize $ \(a, b) -> f a + g b
180
181getRemainingEncrypted :: Get (Encrypted a)
182getRemainingEncrypted = Encrypted <$> (remaining >>= getBytes)
183
184putEncrypted :: Encrypted a -> Put
185putEncrypted (Encrypted bs) = putByteString bs
186
187newtype Plain (s:: * -> Constraint) a = Plain ByteString
188
189
190decodePlain :: Serialize a => Plain Serialize a -> Either String a
191decodePlain (Plain bs) = decode bs
192
193encodePlain :: Serialize a => a -> Plain Serialize a
194encodePlain a = Plain $ encode a
195
196storePlain :: Storable a => a -> IO (Plain Storable a)
197storePlain a = Plain <$> BA.create (sizeOf a) (`poke` a)
198
199retrievePlain :: Storable a => Plain Storable a -> IO a
200retrievePlain (Plain bs) = BA.withByteArray bs peek
201
202decryptSymmetric :: SymmetricKey -> Nonce24 -> Encrypted a -> Either String (Plain s a)
203decryptSymmetric (SymmetricKey symmkey) (Nonce24 n24) (Encrypted bs) = do
204 let sym_nonce_bytes = B.take 12 n24
205 (mac, bs'') = B.splitAt 16 bs
206 symm <- left show . Cryptonite.eitherCryptoError $ do
207 sym_nonce <- Symmetric.nonce12 sym_nonce_bytes
208 Symmetric.initialize symmkey sym_nonce
209 let (ds, symm') = Symmetric.decrypt bs'' symm
210 auth = Symmetric.finalize symm'
211 if BA.convert auth /= mac
212 then Left "symmetricDecipher: Auth fail."
213 else return $ Plain ds
214
215encryptSymmetric :: SymmetricKey -> Nonce24 -> Plain s x -> Encrypted x
216encryptSymmetric (SymmetricKey symmkey) (Nonce24 n24) (Plain bs) = Encrypted es
217 where
218 Cryptonite.CryptoPassed es = do
219 sym_nonce <- Symmetric.nonce12 (BA.take 12 n24)
220 symm <- Symmetric.initialize symmkey sym_nonce
221 let (rpath_bs, symm') = Symmetric.encrypt bs symm
222 auth = Symmetric.finalize symm' -- 16 bytes
223 return (BA.convert auth `BA.append` rpath_bs)
224
225
226data State = State Poly1305.State XSalsa.State
227
228decrypt :: State -> Encrypted a -> Either String (Plain s a)
229decrypt (State hash crypt) ciphertext
230 | (a == mac) = Right (Plain m)
231 | otherwise = Left "decipherAndAuth: auth fail"
232 where
233 (mac, c) = authAndBytes ciphertext
234 m = fst . XSalsa.combine crypt $ c
235 a = Auth . Poly1305.finalize . Poly1305.update hash $ c
236
237-- Encrypt-then-Mac: Encrypt the cleartext, then compute the MAC on the
238-- ciphertext, and prepend it to the ciphertext
239encrypt :: State -> Plain s a -> Encrypted a
240encrypt (State hash crypt) (Plain m) = Encrypted $ B.append (encode a) c
241 where
242 c = fst . XSalsa.combine crypt $ m
243 a = Auth . Poly1305.finalize . Poly1305.update hash $ c
244
245-- (Poly1305.State, XSalsa.State)
246computeSharedSecret :: SecretKey -> PublicKey -> Nonce24 -> State
247computeSharedSecret sk recipient = k `seq` \nonce ->
248 let -- cipher state
249 st0 = XSalsa.initialize 20 k nonce
250 -- Poly1305 key
251 (rs, crypt) = XSalsa.combine st0 zs where Nonce32 zs = zeros32
252 -- Since rs is 32 bytes, this pattern should never fail...
253 Cryptonite.CryptoPassed hash = Poly1305.initialize rs
254 in State hash crypt
255 where
256 -- diffie helman
257 shared = ecdh (Proxy :: Proxy Curve_X25519) sk recipient
258 -- shared secret XSalsa key
259 k = hsalsa20 shared zeros24
260
261unsafeFirstWord64 :: ByteArrayAccess ba => ba -> Word64
262unsafeFirstWord64 ba = unsafeDupablePerformIO $ BA.withByteArray ba peek
263{-# INLINE unsafeFirstWord64 #-}
264
265instance Hashable PublicKey where
266 hashWithSalt salt pk = hashWithSalt salt (unsafeFirstWord64 pk)
267 {-# INLINE hashWithSalt #-}
268
269instance Hashable SecretKey where
270 hashWithSalt salt sk = hashWithSalt salt (unsafeFirstWord64 sk)
271 {-# INLINE hashWithSalt #-}
272
273instance Ord PublicKey where compare = unsafeCompare32Bytes
274 {-# INLINE compare #-}
275instance Ord SecretKey where compare = unsafeCompare32Bytes
276 {-# INLINE compare #-}
277
278unsafeCompare32Bytes :: (ByteArrayAccess ba, ByteArrayAccess bb)
279 => ba -> bb -> Ordering
280unsafeCompare32Bytes ba bb =
281 unsafeDupablePerformIO $ BA.withByteArray ba
282 $ \pa -> BA.withByteArray bb
283 $ \pb -> unsafeCompare32Bytes' 3 pa pb
284
285unsafeCompare32Bytes' :: Int -> Ptr Word64 -> Ptr Word64 -> IO Ordering
286unsafeCompare32Bytes' !n !pa !pb = do
287 a <- peek pa
288 b <- peek pb
289 if n == 0
290 then return $! inline compare a b
291 else case inline compare a b of
292 EQ -> unsafeCompare32Bytes' (n - 1)
293 (pa `plusPtr` 8)
294 (pb `plusPtr` 8)
295 neq -> return neq
296
297
298
299lookupSharedSecret :: TransportCrypto -> SecretKey -> PublicKey -> Nonce24 -> IO State
300lookupSharedSecret TransportCrypto{secretsCache} sk recipient nonce = do
301 now <- getPOSIXTime
302 atomically $ do
303 mm <- readTVar $ sharedSecret secretsCache
304 case MM.lookup' recipient mm of
305 Nothing -> do
306 let miss = computeSharedSecret sk recipient
307 writeTVar (sharedSecret secretsCache)
308 (MM.insertTake' 160 recipient (MM.singleton' sk miss (Down now)) (Down now) mm)
309 return $ miss nonce
310 Just (stamp,smm) -> do
311 let (r,v) = case MM.lookup' sk smm of
312 Nothing | let miss = computeSharedSecret sk recipient
313 -> (miss, MM.insertTake' 3 sk miss (Down now) smm)
314 Just (stamp2,hit) -> (hit , MM.insert' sk hit (Down now) smm)
315 writeTVar (sharedSecret secretsCache)
316 (MM.insertTake' 160 recipient v (Down now) mm)
317 return $ r nonce
318
319
320hsalsa20 :: (ByteArrayAccess t, ByteArrayAccess t1) => t1 -> t -> BA.ScrubbedBytes
321hsalsa20 k n = BA.append a b
322 where
323 Salsa.State st = XSalsa.initialize 20 k n
324 (_, as) = BA.splitAt 4 st
325 (a, xs) = BA.splitAt 16 as
326 (_, bs) = BA.splitAt 24 xs
327 (b, _ ) = BA.splitAt 16 bs
328
329
330newtype Nonce24 = Nonce24 ByteString
331 deriving (Eq, Ord, ByteArrayAccess,Data)
332
333addtoNonce24 :: Nonce24 -> Word -> IO Nonce24
334addtoNonce24 (Nonce24 n24) n = Nonce24 <$> BA.copy n24 init
335 where
336 init :: Ptr Word -> IO ()
337 init ptr | fitsInInt (Proxy :: Proxy Word64) = do
338 let frmBE64 = fromIntegral . fromBE64 . fromIntegral
339 tBE64 = fromIntegral . toBE64 . fromIntegral
340 !(W# input) = n
341 W# w1 <- frmBE64 <$> peek ptr
342 W# w2 <- frmBE64 <$> peekElemOff ptr 1
343 W# w3 <- frmBE64 <$> peekElemOff ptr 2
344 let (# overflw, sum #) = plusWord2# w3 input
345 (# overflw', sum' #) = plusWord2# w2 overflw
346 (# discard, sum'' #) = plusWord2# w1 overflw'
347 poke ptr $ tBE64 (W# sum'')
348 pokeElemOff ptr 1 $ tBE64 (W# sum')
349 pokeElemOff ptr 2 $ tBE64 (W# sum)
350
351 init ptr | fitsInInt (Proxy :: Proxy Word32) = do
352 let frmBE32 = fromIntegral . fromBE32 . fromIntegral
353 tBE32 = fromIntegral . toBE32 . fromIntegral
354 !(W# input) = n
355 W# w1 <- frmBE32 <$> peek ptr
356 W# w2 <- frmBE32 <$> peekElemOff ptr 1
357 W# w3 <- frmBE32 <$> peekElemOff ptr 2
358 W# w4 <- frmBE32 <$> peekElemOff ptr 3
359 W# w5 <- frmBE32 <$> peekElemOff ptr 4
360 W# w6 <- frmBE32 <$> peekElemOff ptr 5
361 let (# overflw_, sum_ #) = plusWord2# w6 input
362 (# overflw__, sum__ #) = plusWord2# w5 overflw_
363 (# overflw___, sum___ #) = plusWord2# w6 overflw__
364 (# overflw, sum #) = plusWord2# w3 overflw___
365 (# overflw', sum' #) = plusWord2# w2 overflw
366 (# discard, sum'' #) = plusWord2# w1 overflw'
367 poke ptr $ tBE32 (W# sum'')
368 pokeElemOff ptr 1 $ tBE32 (W# sum')
369 pokeElemOff ptr 2 $ tBE32 (W# sum)
370 pokeElemOff ptr 3 $ tBE32 (W# sum___)
371 pokeElemOff ptr 4 $ tBE32 (W# sum__)
372 pokeElemOff ptr 5 $ tBE32 (W# sum_)
373 init _ = error "incrementNonce24: I only support 64 and 32 bits"
374
375incrementNonce24 :: Nonce24 -> IO Nonce24
376incrementNonce24 nonce24 = addtoNonce24 nonce24 1
377
378quoted :: ShowS -> ShowS
379quoted shows s = '"':shows ('"':s)
380
381bin2hex :: ByteArrayAccess bs => bs -> String
382bin2hex = C8.unpack . Base16.encode . BA.convert
383
384bin2base64 :: ByteArrayAccess bs => bs -> String
385bin2base64 = C8.unpack . Base64.encode . BA.convert
386
387
388instance Show Nonce24 where
389 showsPrec d nonce = quoted (mappend $ bin2hex nonce)
390
391instance Sized Nonce24 where size = ConstSize 24
392
393instance Serialize Nonce24 where
394 get = Nonce24 <$> getBytes 24
395 put (Nonce24 bs) = putByteString bs
396
397newtype Nonce8 = Nonce8 Word64
398 deriving (Eq, Ord, Data, Serialize)
399
400-- Note: Big-endian to match Serialize instance.
401instance Storable Nonce8 where
402 sizeOf _ = 8
403 alignment _ = alignment (undefined::Word64)
404 peek ptr = Nonce8 . fromBE64 <$> peek (castPtr ptr)
405 poke ptr (Nonce8 w) = poke (castPtr ptr) (toBE64 w)
406
407instance Sized Nonce8 where size = ConstSize 8
408
409instance ByteArrayAccess Nonce8 where
410 length _ = 8
411 withByteArray (Nonce8 w64) kont =
412 allocaBytes 8 $ \p -> do
413 poke (castPtr p :: Ptr Word64) $ toBE64 w64
414 kont p
415
416instance Show Nonce8 where
417 showsPrec d nonce = quoted (mappend $ bin2hex nonce)
418
419
420newtype Nonce32 = Nonce32 ByteString
421 deriving (Eq, Ord, ByteArrayAccess, Data)
422
423instance Show Nonce32 where
424 showsPrec d nonce = mappend $ bin2base64 nonce
425
426instance Read Nonce32 where
427 readsPrec _ str = either (const []) id $ do
428 let (ds,ss) = Prelude.splitAt 43 str
429 ss' <- case ss of
430 '=':xs -> Right xs -- optional terminating '='
431 _ -> Right ss
432 bs <- Base64.decode (C8.pack $ ds ++ ['='])
433 guard $ B.length bs == 32
434 return [ (Nonce32 bs, ss') ]
435
436instance Serialize Nonce32 where
437 get = Nonce32 <$> getBytes 32
438 put (Nonce32 bs) = putByteString bs
439
440instance Sized Nonce32 where size = ConstSize 32
441
442
443zeros32 :: Nonce32
444zeros32 = Nonce32 $ BA.replicate 32 0
445
446zeros24 :: ByteString
447zeros24 = BA.take 24 zs where Nonce32 zs = zeros32
448
449-- | `32` | sender's DHT public key |
450-- | `24` | nonce |
451-- | `?` | encrypted message |
452data Asymm a = Asymm
453 { senderKey :: PublicKey
454 , asymmNonce :: Nonce24
455 , asymmData :: a
456 }
457 deriving (Functor,Foldable,Traversable, Show)
458
459instance Sized a => Sized (Asymm a) where
460 size = case size of
461 ConstSize a -> ConstSize $ a + 24 + 32
462 VarSize f -> VarSize $ \Asymm { asymmData = x } -> f x + 24 + 32
463
464-- | Field order: senderKey, then nonce This is the format used by
465-- Ping/Pong/GetNodes/SendNodes.
466--
467-- See 'getAliasedAsymm' if the nonce precedes the key.
468getAsymm :: Serialize a => Get (Asymm a)
469getAsymm = Asymm <$> getPublicKey <*> get <*> get
470
471putAsymm :: Serialize a => Asymm a -> Put
472putAsymm (Asymm key nonce dta) = putPublicKey key >> put nonce >> put dta
473
474-- | Field order: nonce, and then senderKey.
475getAliasedAsymm :: Serialize a => Get (Asymm a)
476getAliasedAsymm = flip Asymm <$> get <*> getPublicKey <*> get
477
478putAliasedAsymm :: Serialize a => Asymm a -> Put
479putAliasedAsymm (Asymm key nonce dta) = put nonce >> putPublicKey key >> put dta
480
481data SecretsCache = SecretsCache
482 { sharedSecret :: TVar (MinMaxPSQ' PublicKey
483 (Down POSIXTime)
484 (MinMaxPSQ' SecretKey (Down POSIXTime) (Nonce24 -> State)))
485 }
486
487newSecretsCache :: IO SecretsCache
488newSecretsCache = atomically (SecretsCache <$> newTVar MM.empty)
489
490
491newtype SymmetricKey = SymmetricKey ByteString
492
493data TransportCrypto = TransportCrypto
494 { transportSecret :: SecretKey
495 , transportPublic :: PublicKey
496 , onionAliasSecret :: SecretKey
497 , onionAliasPublic :: PublicKey
498 , rendezvousSecret :: SecretKey
499 , rendezvousPublic :: PublicKey
500 , transportSymmetric :: STM SymmetricKey
501 , transportNewNonce :: STM Nonce24
502 , userKeys :: TVar [(SecretKey,PublicKey)]
503 , pendingCookies :: TVar [(SockAddr, (Int, PublicKey))]
504 , secretsCache :: SecretsCache
505 }
506
507getPublicKey :: S.Get PublicKey
508getPublicKey = throwCryptoError . publicKey <$> S.getBytes 32
509
510putPublicKey :: PublicKey -> S.Put
511putPublicKey bs = S.putByteString $ BA.convert bs
512
513-- 32 bytes -> 42 base64 digits.
514--
515encodeSecret :: SecretKey -> Maybe C8.ByteString
516encodeSecret k = do
517 (a,bs) <- BA.uncons (BA.convert k)
518 -- Bytes
519 -- 1 31
520 -- a | bs
521 (cs,c) <- unsnoc bs
522 -- Bytes
523 -- 1 30 1
524 -- a | cs | c
525 --
526 -- Based on the following pasted from the generateSecretKey function:
527 --
528 -- tweakToSecretKey :: ScrubbedBytes -> SecretKey
529 -- tweakToSecretKey bin = SecretKey $ B.copyAndFreeze bin $ \inp -> do
530 -- modifyByte inp 0 (\e0 -> e0 .&. 0xf8)
531 -- modifyByte inp 31 (\e31 -> (e31 .&. 0x7f) .|. 0x40)
532 --
533 -- We know the following holds:
534 -- a == a .&. 0xf8
535 -- c == (c .&. 0x7f) .|. 0x40
536 --
537 -- Therefore, there are 5 reserved bits:
538 -- a := aaaa a000
539 -- c := 01dd cccc
540 --
541 -- That gives us 256 - 5 = 251 bits to encode.
542 -- 42 * 6 = 252
543 --
544 let -- We'll reserve the first bit as zero so that the encoded
545 -- key starts with a digit between A and f. Other digits will be
546 -- arbitrary.
547 --
548 -- The middle 30 bytes will be encoded as is from the source byte
549 -- string (cs). It remains to compute the first (a') and last (c')
550 -- bytes.
551 xs = Base64.encode $ a' `BA.cons` cs `BA.snoc` c'
552 -- a' := 0aaaaadd
553 a' = shiftR a 1 .|. (shiftR c 4 .&. 0x03)
554 -- c' := cccc0000
555 c' = shiftL c 4
556 return $ BA.take 42 xs
557
558-- 42 base64 digits. First digit should be between A and f. The rest are
559-- arbitrary.
560decodeSecret :: C8.ByteString -> Maybe SecretKey
561decodeSecret k64 | B.length k64 < 42 = Nothing
562decodeSecret k64 = do
563 xs <- either (const Nothing) Just $ Base64.decode $ B.append k64 "A="
564 (a',ds) <- B.uncons $ B.take 32 xs
565 (cs,c') <- B.unsnoc ds
566 let c = 0x40 .|. shiftR c' 4 .|. ( 0x30 .&. shiftL a' 4)
567 a = 0xf8 .&. shiftL a' 1
568 case secretKey $ B.cons a cs `B.snoc` c of
569 CryptoPassed x -> Just x
570 _ -> Nothing
571
diff --git a/src/Data/BEncode/Pretty.hs b/src/Data/BEncode/Pretty.hs
new file mode 100644
index 00000000..89fcb489
--- /dev/null
+++ b/src/Data/BEncode/Pretty.hs
@@ -0,0 +1,83 @@
1{-# LANGUAGE CPP #-}
2module Data.BEncode.Pretty where -- (showBEncode) where
3
4import Data.BEncode.Types
5import qualified Data.ByteString as BS
6import qualified Data.ByteString.Lazy as BL
7import qualified Data.ByteString.Lazy.Char8 as BL8
8import Data.Text (Text)
9import qualified Data.Text as T
10import Data.Text.Encoding
11import Text.Printf
12import qualified Data.ByteString.Base16 as Base16
13#ifdef BENCODE_AESON
14import Data.BEncode.BDict hiding (map)
15import Data.Aeson.Types hiding (parse)
16import Data.Aeson.Encode.Pretty
17import qualified Data.HashMap.Strict as HashMap
18import qualified Data.Vector as Vector
19import Data.Foldable as Foldable
20#endif
21
22{-
23unhex :: Text -> BS.ByteString
24unhex t = BS.pack $ map unhex1 [0 .. BS.length nibs `div` 2]
25 where
26 nibs = encodeUtf8 t
27 unhex1 i = unnib (BS.index nibs (i * 2)) * 0x10
28 + unnib (BS.index nibs (i * 2 + 1))
29 unnib a | a <= 0x39 = a - 0x30
30 | otherwise = a - (0x41 - 10)
31
32hex :: BS.ByteString -> Text
33hex bs = T.concat $ map (T.pack . printf "%02X") $ BS.unpack bs
34-}
35
36#ifdef BENCODE_AESON
37
38quote_chr :: Char
39quote_chr = ' '
40
41quote :: Text -> Text
42quote t = quote_chr `T.cons` t `T.snoc` quote_chr
43
44encodeByteString :: BS.ByteString -> Text
45encodeByteString s = either (const . decodeUtf8 $ Base16.encode s) quote $ decodeUtf8' s
46
47decodeByteString :: Text -> BS.ByteString
48decodeByteString s
49 | T.head s==quote_chr = encodeUtf8 (T.takeWhile (/=quote_chr) $ T.drop 1 s)
50 | otherwise = fst (Base16.decode (encodeUtf8 s))
51
52instance ToJSON BValue where
53 toJSON (BInteger x) = Number $ fromIntegral x
54 toJSON (BString s) = String $ encodeByteString s
55 toJSON (BList xs) = Array $ Vector.fromList $ map toJSON xs
56 toJSON (BDict d) = toJSON d
57
58instance ToJSON a => ToJSON (BDictMap a) where
59 toJSON d = Object $ HashMap.fromList $ map convert $ toAscList d
60 where
61 convert (k,v) = (encodeByteString k,toJSON v)
62
63instance FromJSON BValue where
64 parseJSON (Number x) = pure $ BInteger (truncate x)
65 parseJSON (Bool x) = pure $ BInteger $ if x then 1 else 0
66 parseJSON (String s) = pure $ BString $ decodeByteString s
67 parseJSON (Array v) = BList <$> traverse parseJSON (Foldable.toList v)
68 parseJSON (Object d) = BDict <$> parseJSON (Object d)
69 parseJSON (Null) = pure $ BDict Nil
70
71instance FromJSON v => FromJSON (BDictMap v) where
72 parseJSON (Object d) = fromAscList <$> traverse convert (HashMap.toList d)
73 where
74 convert (k,v) = (,) (decodeByteString k) <$> parseJSON v
75 parseJSON _ = fail "Not a BDict"
76#endif
77
78showBEncode :: BValue -> BL.ByteString
79#ifdef BENCODE_AESON
80showBEncode b = encodePretty $ toJSON b
81#else
82showBEncode b = BL8.pack (show b)
83#endif
diff --git a/src/Data/Bits/ByteString.hs b/src/Data/Bits/ByteString.hs
new file mode 100644
index 00000000..bf0316fd
--- /dev/null
+++ b/src/Data/Bits/ByteString.hs
@@ -0,0 +1,132 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3-------------------------------------------------------------------------------
4-- |
5-- Module : Data.Bits.ByteString
6-- Copyright : (c) 2016 Michael Carpenter
7-- License : BSD3
8-- Maintainer : Michael Carpenter <oldmanmike.dev@gmail.com>
9-- Stability : experimental
10-- Portability : portable
11--
12-------------------------------------------------------------------------------
13module Data.Bits.ByteString where
14
15import Data.Bits
16import qualified Data.ByteString as B
17import Data.Word
18
19instance Bits B.ByteString where
20
21 (.&.) a b = B.pack $ B.zipWith (.&.) a b
22 {-# INLINE (.&.) #-}
23
24 (.|.) a b = B.pack $ B.zipWith (.|.) a b
25 {-# INLINE (.|.) #-}
26
27 xor a b = B.pack $ B.zipWith xor a b
28 {-# INLINE xor #-}
29
30 complement = B.map complement
31 {-# INLINE complement #-}
32
33 shift x i
34 | i < 0 = x `shiftR` (-i)
35 | i > 0 = x `shiftL` i
36 | otherwise = x
37 {-# INLINE shift #-}
38
39 shiftR bs 0 = bs
40 shiftR "" _ = B.empty
41 shiftR bs i
42 | i `mod` 8 == 0 =
43 B.take (B.length bs) $ B.append
44 (B.replicate (i `div` 8) 0)
45 (B.drop (i `div` 8) bs)
46 | i `mod` 8 /= 0 =
47 B.pack $ take (B.length bs)
48 $ (replicate (i `div` 8) (0 :: Word8))
49 ++ (go (i `mod` 8) 0 $ B.unpack (B.take (B.length bs - (i `div` 8)) bs))
50 where
51 go _ _ [] = []
52 go j w1 (w2:wst) = (maskR j w1 w2) : go j w2 wst
53 maskR j w1 w2 = (shiftL w1 (8-j)) .|. (shiftR w2 j)
54 shiftR _ _ = error "I can't believe you've done this."
55 {-# INLINE shiftR #-}
56
57 shiftL bs 0 = bs
58 shiftL "" _ = B.empty
59 shiftL bs i
60 | i `mod` 8 == 0 =
61 B.take (B.length bs) $ B.append
62 (B.drop (i `div` 8) bs)
63 (B.replicate (i `div` 8) 0)
64 | i `mod` 8 /= 0 =
65 B.pack $ drop ((i `div` 8) - B.length bs)
66 $ (tail (go (i `mod` 8) 0 $ B.unpack (B.drop (i `div` 8) bs)))
67 ++ (replicate (i `div` 8) 0)
68 where
69 go j w1 [] = [shiftL w1 j]
70 go j w1 (w2:wst) = (maskL j w1 w2) : go j w2 wst
71 maskL j w1 w2 = (shiftL w1 j) .|. (shiftR w2 (8-j))
72 shiftL _ _ = error "I can't believe you've done this."
73 {-# INLINE shiftL #-}
74
75 rotate x i
76 | i < 0 = x `rotateR` (-i)
77 | i > 0 = x `rotateL` i
78 | otherwise = x
79 {-# INLINE rotate #-}
80
81 rotateR bs 0 = bs
82 rotateR bs i
83 | B.length bs == 0 = B.empty
84 | B.length bs == 1 = B.singleton (rotateR (bs `B.index` 0) i)
85 | B.length bs > 1 = do
86 let shiftedWords =
87 B.append
88 (B.drop (nWholeWordsToShift i) bs)
89 (B.take (nWholeWordsToShift i) bs)
90 let tmpShiftedBits = (shiftR shiftedWords (i `mod` 8))
91 let rotatedBits = (shiftL (B.last shiftedWords) (8 - (i `mod` 8))) .|. (B.head tmpShiftedBits)
92 rotatedBits `B.cons` (B.tail tmpShiftedBits)
93 where
94 nWholeWordsToShift n = (B.length bs - (n `div` 8))
95 rotateR _ _ = error "I can't believe you've done this."
96 {-# INLINE rotateR #-}
97
98 rotateL bs 0 = bs
99 rotateL bs i
100 | B.length bs == 0 = B.empty
101 | B.length bs == 1 = B.singleton (rotateL (bs `B.index` 0) i)
102 | i `mod` 8 == 0 = B.append
103 (B.drop (i `div` 8) bs)
104 (B.take (i `div` 8) bs)
105 | B.length bs > 1 = do
106 let shiftedWords =
107 B.append
108 (B.drop (i `div` 8) bs)
109 (B.take (i `div` 8) bs)
110 let tmpShiftedBits = (shiftL shiftedWords (i `mod` 8))
111 let rotatedBits = (shiftR (B.head shiftedWords) (8 - (i `mod` 8))) .|. (B.last tmpShiftedBits)
112 (B.init tmpShiftedBits) `B.snoc` rotatedBits
113 rotateL _ _ = error "I can't believe you've done this."
114 {-# INLINE rotateL #-}
115
116 bitSize x = 8 * B.length x
117 {-# INLINE bitSize #-}
118
119 bitSizeMaybe x = Just (8 * B.length x)
120 {-# INLINE bitSizeMaybe #-}
121
122 isSigned _ = False
123 {-# INLINE isSigned #-}
124
125 testBit x i = testBit (B.index x (B.length x - (i `div` 8) - 1)) (i `mod` 8)
126 {-# INLINE testBit #-}
127
128 bit i = (bit $ mod i 8) `B.cons` (B.replicate (div i 8) (255 :: Word8))
129 {-# INLINE bit #-}
130
131 popCount x = sum $ map popCount $ B.unpack x
132 {-# INLINE popCount #-}
diff --git a/src/Data/Digest/CRC32C.hs b/src/Data/Digest/CRC32C.hs
new file mode 100644
index 00000000..18c1314f
--- /dev/null
+++ b/src/Data/Digest/CRC32C.hs
@@ -0,0 +1,100 @@
1module Data.Digest.CRC32C
2 ( crc32c
3 , crc32c_update
4 ) where
5
6import Data.Bits
7import Data.ByteString (ByteString)
8import Data.Word
9import Data.Array.Base (unsafeAt)
10import Data.Array.Unboxed
11
12import qualified Data.ByteString as B
13
14
15crc32c :: ByteString -> Word32
16crc32c = crc32c_update 0
17
18crc32c_update :: Word32 -> ByteString -> Word32
19crc32c_update crc bs = flipd $ step (flipd crc) bs
20 where
21 flipd = xor 0xffffffff
22
23step :: Word32 -> ByteString -> Word32
24step crc bs = B.foldl step' crc bs
25 where
26 step' acc b = let x = table !!! ((acc .&. 0xff) `xor` fromIntegral b)
27 in x `xor` (acc `shiftR` 8)
28{-# INLINEABLE step #-}
29
30(!!!) :: (IArray a e, Ix i, Integral i) => a i e -> i -> e
31arr !!! i = unsafeAt arr $ fromIntegral i
32{-# INLINEABLE (!!!) #-}
33
34table :: UArray Word32 Word32
35table = listArray (0,255) $
36 [ 0x00000000, 0xf26b8303, 0xe13b70f7, 0x1350f3f4
37 , 0xc79a971f, 0x35f1141c, 0x26a1e7e8, 0xd4ca64eb
38 , 0x8ad958cf, 0x78b2dbcc, 0x6be22838, 0x9989ab3b
39 , 0x4d43cfd0, 0xbf284cd3, 0xac78bf27, 0x5e133c24
40 , 0x105ec76f, 0xe235446c, 0xf165b798, 0x030e349b
41 , 0xd7c45070, 0x25afd373, 0x36ff2087, 0xc494a384
42 , 0x9a879fa0, 0x68ec1ca3, 0x7bbcef57, 0x89d76c54
43 , 0x5d1d08bf, 0xaf768bbc, 0xbc267848, 0x4e4dfb4b
44 , 0x20bd8ede, 0xd2d60ddd, 0xc186fe29, 0x33ed7d2a
45 , 0xe72719c1, 0x154c9ac2, 0x061c6936, 0xf477ea35
46 , 0xaa64d611, 0x580f5512, 0x4b5fa6e6, 0xb93425e5
47 , 0x6dfe410e, 0x9f95c20d, 0x8cc531f9, 0x7eaeb2fa
48 , 0x30e349b1, 0xc288cab2, 0xd1d83946, 0x23b3ba45
49 , 0xf779deae, 0x05125dad, 0x1642ae59, 0xe4292d5a
50 , 0xba3a117e, 0x4851927d, 0x5b016189, 0xa96ae28a
51 , 0x7da08661, 0x8fcb0562, 0x9c9bf696, 0x6ef07595
52 , 0x417b1dbc, 0xb3109ebf, 0xa0406d4b, 0x522bee48
53 , 0x86e18aa3, 0x748a09a0, 0x67dafa54, 0x95b17957
54 , 0xcba24573, 0x39c9c670, 0x2a993584, 0xd8f2b687
55 , 0x0c38d26c, 0xfe53516f, 0xed03a29b, 0x1f682198
56 , 0x5125dad3, 0xa34e59d0, 0xb01eaa24, 0x42752927
57 , 0x96bf4dcc, 0x64d4cecf, 0x77843d3b, 0x85efbe38
58 , 0xdbfc821c, 0x2997011f, 0x3ac7f2eb, 0xc8ac71e8
59 , 0x1c661503, 0xee0d9600, 0xfd5d65f4, 0x0f36e6f7
60 , 0x61c69362, 0x93ad1061, 0x80fde395, 0x72966096
61 , 0xa65c047d, 0x5437877e, 0x4767748a, 0xb50cf789
62 , 0xeb1fcbad, 0x197448ae, 0x0a24bb5a, 0xf84f3859
63 , 0x2c855cb2, 0xdeeedfb1, 0xcdbe2c45, 0x3fd5af46
64 , 0x7198540d, 0x83f3d70e, 0x90a324fa, 0x62c8a7f9
65 , 0xb602c312, 0x44694011, 0x5739b3e5, 0xa55230e6
66 , 0xfb410cc2, 0x092a8fc1, 0x1a7a7c35, 0xe811ff36
67 , 0x3cdb9bdd, 0xceb018de, 0xdde0eb2a, 0x2f8b6829
68 , 0x82f63b78, 0x709db87b, 0x63cd4b8f, 0x91a6c88c
69 , 0x456cac67, 0xb7072f64, 0xa457dc90, 0x563c5f93
70 , 0x082f63b7, 0xfa44e0b4, 0xe9141340, 0x1b7f9043
71 , 0xcfb5f4a8, 0x3dde77ab, 0x2e8e845f, 0xdce5075c
72 , 0x92a8fc17, 0x60c37f14, 0x73938ce0, 0x81f80fe3
73 , 0x55326b08, 0xa759e80b, 0xb4091bff, 0x466298fc
74 , 0x1871a4d8, 0xea1a27db, 0xf94ad42f, 0x0b21572c
75 , 0xdfeb33c7, 0x2d80b0c4, 0x3ed04330, 0xccbbc033
76 , 0xa24bb5a6, 0x502036a5, 0x4370c551, 0xb11b4652
77 , 0x65d122b9, 0x97baa1ba, 0x84ea524e, 0x7681d14d
78 , 0x2892ed69, 0xdaf96e6a, 0xc9a99d9e, 0x3bc21e9d
79 , 0xef087a76, 0x1d63f975, 0x0e330a81, 0xfc588982
80 , 0xb21572c9, 0x407ef1ca, 0x532e023e, 0xa145813d
81 , 0x758fe5d6, 0x87e466d5, 0x94b49521, 0x66df1622
82 , 0x38cc2a06, 0xcaa7a905, 0xd9f75af1, 0x2b9cd9f2
83 , 0xff56bd19, 0x0d3d3e1a, 0x1e6dcdee, 0xec064eed
84 , 0xc38d26c4, 0x31e6a5c7, 0x22b65633, 0xd0ddd530
85 , 0x0417b1db, 0xf67c32d8, 0xe52cc12c, 0x1747422f
86 , 0x49547e0b, 0xbb3ffd08, 0xa86f0efc, 0x5a048dff
87 , 0x8ecee914, 0x7ca56a17, 0x6ff599e3, 0x9d9e1ae0
88 , 0xd3d3e1ab, 0x21b862a8, 0x32e8915c, 0xc083125f
89 , 0x144976b4, 0xe622f5b7, 0xf5720643, 0x07198540
90 , 0x590ab964, 0xab613a67, 0xb831c993, 0x4a5a4a90
91 , 0x9e902e7b, 0x6cfbad78, 0x7fab5e8c, 0x8dc0dd8f
92 , 0xe330a81a, 0x115b2b19, 0x020bd8ed, 0xf0605bee
93 , 0x24aa3f05, 0xd6c1bc06, 0xc5914ff2, 0x37faccf1
94 , 0x69e9f0d5, 0x9b8273d6, 0x88d28022, 0x7ab90321
95 , 0xae7367ca, 0x5c18e4c9, 0x4f48173d, 0xbd23943e
96 , 0xf36e6f75, 0x0105ec76, 0x12551f82, 0xe03e9c81
97 , 0x34f4f86a, 0xc69f7b69, 0xd5cf889d, 0x27a40b9e
98 , 0x79b737ba, 0x8bdcb4b9, 0x988c474d, 0x6ae7c44e
99 , 0xbe2da0a5, 0x4c4623a6, 0x5f16d052, 0xad7d5351
100 ]
diff --git a/src/Data/MinMaxPSQ.hs b/src/Data/MinMaxPSQ.hs
new file mode 100644
index 00000000..3b9a4d6c
--- /dev/null
+++ b/src/Data/MinMaxPSQ.hs
@@ -0,0 +1,99 @@
1{-# LANGUAGE BangPatterns, PatternSynonyms #-}
2module Data.MinMaxPSQ
3 ( module Data.MinMaxPSQ
4 , Binding'
5 , pattern Binding
6 ) where
7
8import Data.Ord
9import qualified Data.Wrapper.PSQ as PSQ
10 ;import Data.Wrapper.PSQ as PSQ hiding (insert, insert', null, size)
11import Prelude hiding (null, take)
12
13data MinMaxPSQ' k p v = MinMaxPSQ !(PSQ' k p v) !(PSQ' k (Down p) v)
14type MinMaxPSQ k p = MinMaxPSQ' k p ()
15
16empty :: MinMaxPSQ' k p v
17empty = MinMaxPSQ PSQ.empty PSQ.empty
18
19singleton' :: (PSQKey k, Ord p) => k -> v -> p -> MinMaxPSQ' k p v
20singleton' k v p = MinMaxPSQ (PSQ.singleton' k v p) (PSQ.singleton' k v (Down p))
21
22null :: MinMaxPSQ' k p v -> Bool
23null (MinMaxPSQ nq xq) = PSQ.null nq
24
25size :: MinMaxPSQ' k p v -> Int
26size (MinMaxPSQ nq xq) = PSQ.size nq
27
28toList :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> [Binding' k p v]
29toList (MinMaxPSQ nq xq) = PSQ.toList nq
30
31fromList :: (PSQKey k, Ord p) => [Binding' k p v] -> MinMaxPSQ' k p v
32fromList kps = MinMaxPSQ (PSQ.fromList kps)
33 (PSQ.fromList $ map (\(Binding k v p) -> Binding k v (Down p)) kps)
34
35findMin :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v)
36findMin (MinMaxPSQ nq xq) = PSQ.findMin nq
37
38findMax :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v)
39findMax (MinMaxPSQ nq xq) = fmap (\(Binding k v (Down p)) -> Binding k v p) $ PSQ.findMin xq
40
41insert :: (PSQKey k, Ord p) => k -> p -> MinMaxPSQ k p -> MinMaxPSQ k p
42insert k p (MinMaxPSQ nq xq) = MinMaxPSQ (PSQ.insert k p nq)
43 (PSQ.insert k (Down p) xq)
44
45insert' :: (PSQKey k, Ord p) => k -> v -> p -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v
46insert' k v p (MinMaxPSQ nq xq) = MinMaxPSQ (PSQ.insert' k v p nq)
47 (PSQ.insert' k v (Down p) xq)
48
49delete :: (PSQKey k, Ord p) => k -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v
50delete k (MinMaxPSQ nq xq) = MinMaxPSQ (PSQ.delete k nq) (PSQ.delete k xq)
51
52deleteMin :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> MinMaxPSQ' k p v
53deleteMin (MinMaxPSQ nq xq) = case PSQ.minView nq of
54 Just (Binding k _ _, nq') -> MinMaxPSQ nq' (PSQ.delete k xq)
55 Nothing -> MinMaxPSQ nq xq
56
57deleteMax :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> MinMaxPSQ' k p v
58deleteMax (MinMaxPSQ nq xq) = case PSQ.minView xq of
59 Just (Binding k _ _, xq') -> MinMaxPSQ (PSQ.delete k nq) xq'
60 Nothing -> MinMaxPSQ nq xq
61
62minView :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v, MinMaxPSQ' k p v)
63minView (MinMaxPSQ nq xq) = fmap (\(Binding k v p, nq') -> (Binding k v p, MinMaxPSQ nq' (PSQ.delete k xq)))
64 $ PSQ.minView nq
65
66maxView :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v, MinMaxPSQ' k p v)
67maxView (MinMaxPSQ nq xq) = fmap (\(Binding k v (Down p), xq') -> (Binding k v p, MinMaxPSQ (PSQ.delete k nq) xq'))
68 $ PSQ.minView xq
69
70-- | Maintains a bounded 'MinMaxPSQ' by deleting the maximum element if the
71-- insertion would cause the queue to have too many elements.
72insertTake :: (PSQKey k, Ord p) => Int -> k -> p -> MinMaxPSQ k p -> MinMaxPSQ k p
73insertTake n k p q = take n $ insert k p q
74
75-- | Maintains a bounded 'MinMaxPSQ\'' by deleting the maximum element if the
76-- insertion would cause the queue to have too many elements.
77insertTake' :: (PSQKey k, Ord p) => Int -> k -> v -> p -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v
78insertTake' n k v p q = take n $ insert' k v p q
79
80
81-- | Truncate the 'MinMaxPSQ' to the given number of lowest priority elements.
82take :: (PSQKey k, Ord p) => Int -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v
83take !n !q | (size q <= n) = q
84 | null q = q
85 | otherwise = take n $ deleteMax q
86
87-- | Like 'take', except it provides a list deleted bindings.
88takeView :: (PSQKey k, Ord p) => Int -> MinMaxPSQ' k p v -> ( [Binding' k p v], MinMaxPSQ' k p v )
89takeView !n !q | (size q <= n) = ([], q)
90 | null q = ([], q)
91 | otherwise = let Just (x,q') = maxView q
92 (xs,q'') = takeView n q'
93 ys = x:xs
94 in (ys, ys `seq` q'')
95
96
97
98lookup' :: (PSQKey k, Ord p) => k -> MinMaxPSQ' k p v -> Maybe (p, v)
99lookup' k (MinMaxPSQ q _) = PSQ.lookup k q
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs
new file mode 100644
index 00000000..4af583ed
--- /dev/null
+++ b/src/Data/Torrent.hs
@@ -0,0 +1,1328 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Torrent file contains metadata about files and folders but not
9-- content itself. The files are bencoded dictionaries. There is
10-- also other info which is used to help join the swarm.
11--
12-- This module provides torrent metainfo serialization and info hash
13-- extraction.
14--
15-- For more info see:
16-- <http://www.bittorrent.org/beps/bep_0003.html#metainfo-files>,
17-- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure>
18--
19{-# LANGUAGE CPP #-}
20{-# LANGUAGE NamedFieldPuns #-}
21{-# LANGUAGE FlexibleInstances #-}
22{-# LANGUAGE MultiParamTypeClasses #-}
23{-# LANGUAGE BangPatterns #-}
24{-# LANGUAGE GeneralizedNewtypeDeriving #-}
25{-# LANGUAGE StandaloneDeriving #-}
26{-# LANGUAGE DeriveDataTypeable #-}
27{-# LANGUAGE DeriveFunctor #-}
28{-# LANGUAGE DeriveFoldable #-}
29{-# LANGUAGE DeriveTraversable #-}
30{-# LANGUAGE TemplateHaskell #-}
31{-# OPTIONS -fno-warn-orphans #-}
32module Data.Torrent
33 ( -- * InfoHash
34 -- $infohash
35 InfoHash(..)
36 , textToInfoHash
37 , longHex
38 , shortHex
39
40 -- * File layout
41 -- ** FileInfo
42 , FileOffset
43 , FileSize
44 , FileInfo (..)
45 , fileLength
46 , filePath
47 , fileMD5Sum
48
49 -- ** Layout info
50 , LayoutInfo (..)
51 , singleFile
52 , multiFile
53 , rootDirName
54 , joinFilePath
55 , isSingleFile
56 , isMultiFile
57 , suggestedName
58 , contentLength
59 , fileCount
60 , blockCount
61
62 -- ** Flat layout info
63 , FileLayout
64 , flatLayout
65 , accumPositions
66 , fileOffset
67
68 -- ** Internal
69 , sizeInBase
70
71 -- * Pieces
72 -- ** Attributes
73 , PieceIx
74 , PieceCount
75 , PieceSize
76 , minPieceSize
77 , maxPieceSize
78 , defaultPieceSize
79 , PieceHash
80
81 -- ** Piece data
82 , Piece (..)
83 , pieceSize
84 , hashPiece
85
86 -- ** Piece control
87 , HashList (..)
88 , PieceInfo (..)
89 , pieceLength
90 , pieceHashes
91 , pieceCount
92
93 -- ** Validation
94 , pieceHash
95 , checkPieceLazy
96
97 -- * Info dictionary
98 , InfoDict (..)
99 , infohash
100 , layoutInfo
101 , pieceInfo
102 , isPrivate
103#ifdef VERSION_bencoding
104 , infoDictionary
105#endif
106
107 -- * Torrent file
108 , Torrent(..)
109
110 -- ** Lenses
111 , announce
112 , announceList
113 , comment
114 , createdBy
115 , creationDate
116 , encoding
117 , infoDict
118 , publisher
119 , publisherURL
120 , signature
121
122 -- ** Utils
123 , nullTorrent
124 , typeTorrent
125 , torrentExt
126 , isTorrentPath
127#ifdef VERSION_bencoding
128 , fromFile
129 , toFile
130#endif
131
132 -- * Magnet
133 -- $magnet-link
134 , Magnet(..)
135 , nullMagnet
136 , simpleMagnet
137 , detailedMagnet
138 , parseMagnet
139 , renderMagnet
140
141 -- ** URN
142 , URN (..)
143 , NamespaceId
144 , btih
145 , infohashURN
146 , parseURN
147 , renderURN
148 ) where
149
150import Prelude
151import Control.Applicative
152import Control.DeepSeq
153import Control.Exception
154import Control.Lens
155import Control.Monad
156import Crypto.Hash
157#ifdef VERSION_bencoding
158import Data.BEncode as BE
159import Data.BEncode.Types as BE
160#endif
161import Data.Bits
162#ifdef VERSION_bits_extras
163import Data.Bits.Extras
164#endif
165import qualified Data.ByteArray as Bytes
166import Data.ByteString as BS
167import Data.ByteString.Base16 as Base16
168import Data.ByteString.Base32 as Base32
169import Data.ByteString.Base64 as Base64
170import Data.ByteString.Char8 as BC (pack, unpack)
171import Data.ByteString.Lazy as BL
172import Data.Char
173import Data.Convertible
174import Data.Default
175import Data.Hashable as Hashable
176import Data.Int
177import Data.List as L
178import Data.Map as M
179import Data.Maybe
180import Data.Serialize as S
181import Data.String
182import Data.Text as T
183import Data.Text.Encoding as T
184import Data.Text.Read
185import Data.Time.Clock.POSIX
186import Data.Typeable
187import Network (HostName)
188import Network.HTTP.Types.QueryLike
189import Network.HTTP.Types.URI
190import Network.URI
191import Text.ParserCombinators.ReadP as P
192import Text.PrettyPrint as PP
193import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
194import System.FilePath
195import System.Posix.Types
196
197import Network.Address
198import Network.Kademlia.Routing
199
200
201{-----------------------------------------------------------------------
202-- Info hash
203-----------------------------------------------------------------------}
204-- TODO
205--
206-- data Word160 = Word160 {-# UNPACK #-} !Word64
207-- {-# UNPACK #-} !Word64
208-- {-# UNPACK #-} !Word32
209--
210-- newtype InfoHash = InfoHash Word160
211--
212-- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes
213
214-- $infohash
215--
216-- Infohash is a unique identifier of torrent.
217
218-- | Exactly 20 bytes long SHA1 hash of the info part of torrent file.
219newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
220 deriving (Eq, Ord, Typeable)
221
222infoHashLen :: Int
223infoHashLen = 20
224
225-- | Meaningless placeholder value.
226instance Default InfoHash where
227 def = "0123456789012345678901234567890123456789"
228
229-- | Hash raw bytes. (no encoding)
230instance Hashable InfoHash where
231 hashWithSalt s (InfoHash ih) = hashWithSalt s ih
232 {-# INLINE hashWithSalt #-}
233
234#ifdef VERSION_bencoding
235-- | Convert to\/from raw bencoded string. (no encoding)
236instance BEncode InfoHash where
237 toBEncode = toBEncode . getInfoHash
238 fromBEncode be = InfoHash <$> fromBEncode be
239#endif
240
241#if 0
242instance TableKey KMessageOf InfoHash where
243 toNodeId = either (error msg) id . S.decode . S.encode
244 where -- TODO unsafe coerse?
245 msg = "tableKey: impossible"
246#endif
247
248
249-- | Convert to\/from raw bytestring. (no encoding)
250instance Serialize InfoHash where
251 put (InfoHash ih) = putByteString ih
252 {-# INLINE put #-}
253
254 get = InfoHash <$> getBytes infoHashLen
255 {-# INLINE get #-}
256
257-- | Convert to raw query value. (no encoding)
258instance QueryValueLike InfoHash where
259 toQueryValue (InfoHash ih) = Just ih
260 {-# INLINE toQueryValue #-}
261
262-- | Convert to base16 encoded string.
263instance Show InfoHash where
264 show (InfoHash ih) = BC.unpack (Base16.encode ih)
265
266-- | Convert to base16 encoded Doc string.
267instance Pretty InfoHash where
268 pPrint = text . show
269
270-- | Read base16 encoded string.
271instance Read InfoHash where
272 readsPrec _ = readP_to_S $ do
273 str <- replicateM (infoHashLen * 2) (satisfy isHexDigit)
274 return $ InfoHash $ decodeIH str
275 where
276 decodeIH = BS.pack . L.map fromHex . pair
277 fromHex (a, b) = read $ '0' : 'x' : a : b : []
278
279 pair (a : b : xs) = (a, b) : pair xs
280 pair _ = []
281
282-- | Convert raw bytes to info hash.
283instance Convertible BS.ByteString InfoHash where
284 safeConvert bs
285 | BS.length bs == infoHashLen = pure (InfoHash bs)
286 | otherwise = convError "invalid length" bs
287
288-- | Parse infohash from base16\/base32\/base64 encoded string.
289instance Convertible Text InfoHash where
290 safeConvert t
291 | 20 == hashLen = pure (InfoHash hashStr)
292 | 26 <= hashLen && hashLen <= 28 =
293 case Base64.decode hashStr of
294 Left msg -> convError ("invalid base64 encoding " ++ msg) t
295 Right ihStr -> safeConvert ihStr
296
297 | hashLen == 32 =
298 case Base32.decode hashStr of
299 Left msg -> convError msg t
300 Right ihStr -> safeConvert ihStr
301
302 | hashLen == 40 =
303 let (ihStr, inv) = Base16.decode hashStr
304 in if BS.length inv /= 0
305 then convError "invalid base16 encoding" t
306 else safeConvert ihStr
307
308 | otherwise = convError "invalid length" t
309 where
310 hashLen = BS.length hashStr
311 hashStr = T.encodeUtf8 t
312
313-- | Decode from base16\/base32\/base64 encoded string.
314instance IsString InfoHash where
315 fromString = either (error . prettyConvertError) id . safeConvert . T.pack
316
317ignoreErrorMsg :: Either a b -> Maybe b
318ignoreErrorMsg = either (const Nothing) Just
319
320-- | Tries both base16 and base32 while decoding info hash.
321--
322-- Use 'safeConvert' for detailed error messages.
323--
324textToInfoHash :: Text -> Maybe InfoHash
325textToInfoHash = ignoreErrorMsg . safeConvert
326
327-- | Hex encode infohash to text, full length.
328longHex :: InfoHash -> Text
329longHex = T.decodeUtf8 . Base16.encode . getInfoHash
330
331-- | The same as 'longHex', but only first 7 characters.
332shortHex :: InfoHash -> Text
333shortHex = T.take 7 . longHex
334
335{-----------------------------------------------------------------------
336-- File info
337-----------------------------------------------------------------------}
338
339-- | Size of a file in bytes.
340type FileSize = FileOffset
341
342#ifdef VERSION_bencoding
343deriving instance BEncode FileOffset
344#endif
345
346-- | Contain metainfo about one single file.
347data FileInfo a = FileInfo {
348 fiLength :: {-# UNPACK #-} !FileSize
349 -- ^ Length of the file in bytes.
350
351 -- TODO unpacked MD5 sum
352 , fiMD5Sum :: !(Maybe BS.ByteString)
353 -- ^ 32 character long MD5 sum of the file. Used by third-party
354 -- tools, not by bittorrent protocol itself.
355
356 , fiName :: !a
357 -- ^ One or more string elements that together represent the
358 -- path and filename. Each element in the list corresponds to
359 -- either a directory name or (in the case of the last element)
360 -- the filename. For example, the file:
361 --
362 -- > "dir1/dir2/file.ext"
363 --
364 -- would consist of three string elements:
365 --
366 -- > ["dir1", "dir2", "file.ext"]
367 --
368 } deriving (Show, Read, Eq, Typeable
369 , Functor, Foldable
370 )
371
372makeLensesFor
373 [ ("fiLength", "fileLength")
374 , ("fiMD5Sum", "fileMD5Sum")
375 , ("fiName" , "filePath" )
376 ]
377 ''FileInfo
378
379instance NFData a => NFData (FileInfo a) where
380 rnf FileInfo {..} = rnf fiName
381 {-# INLINE rnf #-}
382
383#ifdef VERSION_bencoding
384instance BEncode (FileInfo [BS.ByteString]) where
385 toBEncode FileInfo {..} = toDict $
386 "length" .=! fiLength
387 .: "md5sum" .=? fiMD5Sum
388 .: "path" .=! fiName
389 .: endDict
390 {-# INLINE toBEncode #-}
391
392 fromBEncode = fromDict $ do
393 FileInfo <$>! "length"
394 <*>? "md5sum"
395 <*>! "path"
396 {-# INLINE fromBEncode #-}
397
398type Put a = a -> BDict -> BDict
399#endif
400
401#ifdef VERSION_bencoding
402putFileInfoSingle :: Data.Torrent.Put (FileInfo BS.ByteString)
403putFileInfoSingle FileInfo {..} cont =
404 "length" .=! fiLength
405 .: "md5sum" .=? fiMD5Sum
406 .: "name" .=! fiName
407 .: cont
408
409getFileInfoSingle :: BE.Get (FileInfo BS.ByteString)
410getFileInfoSingle = do
411 FileInfo <$>! "length"
412 <*>? "md5sum"
413 <*>! "name"
414
415instance BEncode (FileInfo BS.ByteString) where
416 toBEncode = toDict . (`putFileInfoSingle` endDict)
417 {-# INLINE toBEncode #-}
418
419 fromBEncode = fromDict getFileInfoSingle
420 {-# INLINE fromBEncode #-}
421#endif
422
423instance Pretty (FileInfo BS.ByteString) where
424 pPrint FileInfo {..} =
425 "Path: " <> text (T.unpack (T.decodeUtf8 fiName))
426 $$ "Size: " <> text (show fiLength)
427 $$ maybe PP.empty ppMD5 fiMD5Sum
428 where
429 ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5))
430
431-- | Join file path.
432joinFilePath :: FileInfo [BS.ByteString] -> FileInfo BS.ByteString
433joinFilePath = fmap (BS.intercalate "/")
434
435{-----------------------------------------------------------------------
436-- Layout info
437-----------------------------------------------------------------------}
438
439-- | Original (found in torrent file) layout info is either:
440--
441-- * Single file with its /name/.
442--
443-- * Multiple files with its relative file /paths/.
444--
445data LayoutInfo
446 = SingleFile
447 { -- | Single file info.
448 liFile :: !(FileInfo BS.ByteString)
449 }
450 | MultiFile
451 { -- | List of the all files that torrent contains.
452 liFiles :: ![FileInfo [BS.ByteString]]
453
454 -- | The /suggested/ name of the root directory in which to
455 -- store all the files.
456 , liDirName :: !BS.ByteString
457 } deriving (Show, Read, Eq, Typeable)
458
459makeLensesFor
460 [ ("liFile" , "singleFile" )
461 , ("liFiles" , "multiFile" )
462 , ("liDirName", "rootDirName")
463 ]
464 ''LayoutInfo
465
466instance NFData LayoutInfo where
467 rnf SingleFile {..} = ()
468 rnf MultiFile {..} = rnf liFiles
469
470-- | Empty multifile layout.
471instance Default LayoutInfo where
472 def = MultiFile [] ""
473
474#ifdef VERSION_bencoding
475getLayoutInfo :: BE.Get LayoutInfo
476getLayoutInfo = single <|> multi
477 where
478 single = SingleFile <$> getFileInfoSingle
479 multi = MultiFile <$>! "files" <*>! "name"
480
481putLayoutInfo :: Data.Torrent.Put LayoutInfo
482putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
483putLayoutInfo MultiFile {..} = \ cont ->
484 "files" .=! liFiles
485 .: "name" .=! liDirName
486 .: cont
487
488instance BEncode LayoutInfo where
489 toBEncode = toDict . (`putLayoutInfo` endDict)
490 fromBEncode = fromDict getLayoutInfo
491#endif
492
493instance Pretty LayoutInfo where
494 pPrint SingleFile {..} = pPrint liFile
495 pPrint MultiFile {..} = vcat $ L.map (pPrint . joinFilePath) liFiles
496
497-- | Test if this is single file torrent.
498isSingleFile :: LayoutInfo -> Bool
499isSingleFile SingleFile {} = True
500isSingleFile _ = False
501{-# INLINE isSingleFile #-}
502
503-- | Test if this is multifile torrent.
504isMultiFile :: LayoutInfo -> Bool
505isMultiFile MultiFile {} = True
506isMultiFile _ = False
507{-# INLINE isMultiFile #-}
508
509-- | Get name of the torrent based on the root path piece.
510suggestedName :: LayoutInfo -> BS.ByteString
511suggestedName (SingleFile FileInfo {..}) = fiName
512suggestedName MultiFile {..} = liDirName
513{-# INLINE suggestedName #-}
514
515-- | Find sum of sizes of the all torrent files.
516contentLength :: LayoutInfo -> FileSize
517contentLength SingleFile { liFile = FileInfo {..} } = fiLength
518contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
519
520-- | Get number of all files in torrent.
521fileCount :: LayoutInfo -> Int
522fileCount SingleFile {..} = 1
523fileCount MultiFile {..} = L.length liFiles
524
525-- | Find number of blocks of the specified size. If torrent size is
526-- not a multiple of block size then the count is rounded up.
527blockCount :: Int -> LayoutInfo -> Int
528blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
529
530------------------------------------------------------------------------
531
532-- | File layout specifies the order and the size of each file in the
533-- storage. Note that order of files is highly important since we
534-- coalesce all the files in the given order to get the linear block
535-- address space.
536--
537type FileLayout a = [(FilePath, a)]
538
539-- | Extract files layout from torrent info with the given root path.
540flatLayout
541 :: FilePath -- ^ Root path for the all torrent files.
542 -> LayoutInfo -- ^ Torrent content information.
543 -> FileLayout FileSize -- ^ The all file paths prefixed with the given root.
544flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
545 = [(prefixPath </> BC.unpack fiName, fiLength)]
546flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
547 where -- TODO use utf8 encoding in name
548 mkPath FileInfo {..} = (_path, fiLength)
549 where
550 _path = prefixPath </> BC.unpack liDirName
551 </> joinPath (L.map BC.unpack fiName)
552
553-- | Calculate offset of each file based on its length, incrementally.
554accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
555accumPositions = go 0
556 where
557 go !_ [] = []
558 go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs
559
560-- | Gives global offset of a content file for a given full path.
561fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
562fileOffset = L.lookup
563{-# INLINE fileOffset #-}
564
565------------------------------------------------------------------------
566
567-- | Divide and round up.
568sizeInBase :: Integral a => a -> Int -> Int
569sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
570 where
571 align = if n `mod` fromIntegral b == 0 then 0 else 1
572{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
573{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}
574
575{-----------------------------------------------------------------------
576-- Piece attributes
577-----------------------------------------------------------------------}
578
579-- | Zero-based index of piece in torrent content.
580type PieceIx = Int
581
582-- | Size of piece in bytes. Should be a power of 2.
583--
584-- NOTE: Have max and min size constrained to wide used
585-- semi-standard values. This bounds should be used to make decision
586-- about piece size for new torrents.
587--
588type PieceSize = Int
589
590-- | Number of pieces in torrent or a part of torrent.
591type PieceCount = Int
592
593defaultBlockSize :: Int
594defaultBlockSize = 16 * 1024
595
596-- | Optimal number of pieces in torrent.
597optimalPieceCount :: PieceCount
598optimalPieceCount = 1000
599{-# INLINE optimalPieceCount #-}
600
601-- | Piece size should not be less than this value.
602minPieceSize :: Int
603minPieceSize = defaultBlockSize * 4
604{-# INLINE minPieceSize #-}
605
606-- | To prevent transfer degradation piece size should not exceed this
607-- value.
608maxPieceSize :: Int
609maxPieceSize = 4 * 1024 * 1024
610{-# INLINE maxPieceSize #-}
611
612toPow2 :: Int -> Int
613#ifdef VERSION_bits_extras
614toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
615#else
616toPow2 x = bit $ fromIntegral (countLeadingZeros (0 :: Int) - countLeadingZeros x)
617#endif
618
619-- | Find the optimal piece size for a given torrent size.
620defaultPieceSize :: Int64 -> Int
621defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
622 where
623 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
624
625{-----------------------------------------------------------------------
626-- Piece data
627-----------------------------------------------------------------------}
628
629type PieceHash = BS.ByteString
630
631hashsize :: Int
632hashsize = 20
633{-# INLINE hashsize #-}
634
635-- TODO check if pieceLength is power of 2
636-- | Piece payload should be strict or lazy bytestring.
637data Piece a = Piece
638 { -- | Zero-based piece index in torrent.
639 pieceIndex :: {-# UNPACK #-} !PieceIx
640
641 -- | Payload.
642 , pieceData :: !a
643 } deriving (Show, Read, Eq, Functor, Typeable)
644
645instance NFData a => NFData (Piece a) where
646 rnf (Piece a b) = rnf a `seq` rnf b
647
648-- | Payload bytes are omitted.
649instance Pretty (Piece a) where
650 pPrint Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
651
652-- | Get size of piece in bytes.
653pieceSize :: Piece BL.ByteString -> PieceSize
654pieceSize Piece {..} = fromIntegral (BL.length pieceData)
655
656-- | Get piece hash.
657hashPiece :: Piece BL.ByteString -> PieceHash
658hashPiece Piece {..} = Bytes.convert (hashlazy pieceData :: Digest SHA1)
659
660{-----------------------------------------------------------------------
661-- Piece control
662-----------------------------------------------------------------------}
663
664-- | A flat array of SHA1 hash for each piece.
665newtype HashList = HashList { unHashList :: BS.ByteString }
666 deriving ( Show, Read, Eq, Typeable
667#ifdef VERSION_bencoding
668 , BEncode
669#endif
670 )
671
672-- | Empty hash list.
673instance Default HashList where
674 def = HashList ""
675
676-- | Part of torrent file used for torrent content validation.
677data PieceInfo = PieceInfo
678 { piPieceLength :: {-# UNPACK #-} !PieceSize
679 -- ^ Number of bytes in each piece.
680
681 , piPieceHashes :: !HashList
682 -- ^ Concatenation of all 20-byte SHA1 hash values.
683 } deriving (Show, Read, Eq, Typeable)
684
685-- | Number of bytes in each piece.
686makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
687
688-- | Concatenation of all 20-byte SHA1 hash values.
689makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
690
691instance NFData PieceInfo where
692 rnf (PieceInfo a (HashList b)) = rnf a `seq` rnf b
693
694instance Default PieceInfo where
695 def = PieceInfo 1 def
696
697
698#ifdef VERSION_bencoding
699putPieceInfo :: Data.Torrent.Put PieceInfo
700putPieceInfo PieceInfo {..} cont =
701 "piece length" .=! piPieceLength
702 .: "pieces" .=! piPieceHashes
703 .: cont
704
705getPieceInfo :: BE.Get PieceInfo
706getPieceInfo = do
707 PieceInfo <$>! "piece length"
708 <*>! "pieces"
709
710instance BEncode PieceInfo where
711 toBEncode = toDict . (`putPieceInfo` endDict)
712 fromBEncode = fromDict getPieceInfo
713#endif
714
715-- | Hashes are omitted.
716instance Pretty PieceInfo where
717 pPrint PieceInfo {..} = "Piece size: " <> int piPieceLength
718
719slice :: Int -> Int -> BS.ByteString -> BS.ByteString
720slice start len = BS.take len . BS.drop start
721{-# INLINE slice #-}
722
723-- | Extract validation hash by specified piece index.
724pieceHash :: PieceInfo -> PieceIx -> PieceHash
725pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize (unHashList piPieceHashes)
726
727-- | Find count of pieces in the torrent. If torrent size is not a
728-- multiple of piece size then the count is rounded up.
729pieceCount :: PieceInfo -> PieceCount
730pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize
731
732-- | Test if this is last piece in torrent content.
733isLastPiece :: PieceInfo -> PieceIx -> Bool
734isLastPiece ci i = pieceCount ci == succ i
735
736-- | Validate piece with metainfo hash.
737checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
738checkPieceLazy pinfo @ PieceInfo {..} Piece {..}
739 = (fromIntegral (BL.length pieceData) == piPieceLength
740 || isLastPiece pinfo pieceIndex)
741 && Bytes.convert (hashlazy pieceData :: Digest SHA1) == pieceHash pinfo pieceIndex
742
743{-----------------------------------------------------------------------
744-- Info dictionary
745-----------------------------------------------------------------------}
746
747{- note that info hash is actually reduntant field
748 but it's better to keep it here to avoid heavy recomputations
749-}
750
751-- | Info part of the .torrent file contain info about each content file.
752data InfoDict = InfoDict
753 { idInfoHash :: !InfoHash
754 -- ^ SHA1 hash of the (other) 'DictInfo' fields.
755
756 , idLayoutInfo :: !LayoutInfo
757 -- ^ File layout (name, size, etc) information.
758
759 , idPieceInfo :: !PieceInfo
760 -- ^ Content validation information.
761
762 , idPrivate :: !Bool
763 -- ^ If set the client MUST publish its presence to get other
764 -- peers ONLY via the trackers explicity described in the
765 -- metainfo file.
766 --
767 -- BEP 27: <http://www.bittorrent.org/beps/bep_0027.html>
768 } deriving (Show, Read, Eq, Typeable)
769
770makeLensesFor
771 [ ("idInfoHash" , "infohash" )
772 , ("idLayoutInfo", "layoutInfo")
773 , ("idPieceInfo" , "pieceInfo" )
774 , ("idPrivate" , "isPrivate" )
775 ]
776 ''InfoDict
777
778instance NFData InfoDict where
779 rnf InfoDict {..} = rnf idLayoutInfo
780
781instance Hashable InfoDict where
782 hashWithSalt = Hashable.hashUsing idInfoHash
783 {-# INLINE hashWithSalt #-}
784
785-- | Hash lazy bytestring using SHA1 algorithm.
786hashLazyIH :: BL.ByteString -> InfoHash
787hashLazyIH = either (const (error msg)) id . safeConvert . (Bytes.convert :: Digest SHA1 -> BS.ByteString) . hashlazy
788 where
789 msg = "Infohash.hash: impossible: SHA1 is always 20 bytes long"
790
791#ifdef VERSION_bencoding
792-- | Empty info dictionary with zero-length content.
793instance Default InfoDict where
794 def = infoDictionary def def False
795
796-- | Smart constructor: add a info hash to info dictionary.
797infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict
798infoDictionary li pinfo private = InfoDict ih li pinfo private
799 where
800 ih = hashLazyIH $ BE.encode $ InfoDict def li pinfo private
801
802getPrivate :: BE.Get Bool
803getPrivate = (Just True ==) <$>? "private"
804
805putPrivate :: Bool -> BDict -> BDict
806putPrivate False = id
807putPrivate True = \ cont -> "private" .=! True .: cont
808
809instance BEncode InfoDict where
810 toBEncode InfoDict {..} = toDict $
811 putLayoutInfo idLayoutInfo $
812 putPieceInfo idPieceInfo $
813 putPrivate idPrivate $
814 endDict
815
816 fromBEncode dict = (`fromDict` dict) $ do
817 InfoDict ih <$> getLayoutInfo
818 <*> getPieceInfo
819 <*> getPrivate
820 where
821 ih = hashLazyIH (BE.encode dict)
822#endif
823
824ppPrivacy :: Bool -> Doc
825ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public"
826
827--ppAdditionalInfo :: InfoDict -> Doc
828--ppAdditionalInfo layout = PP.empty
829
830instance Pretty InfoDict where
831 pPrint InfoDict {..} =
832 pPrint idLayoutInfo $$
833 pPrint idPieceInfo $$
834 ppPrivacy idPrivate
835
836{-----------------------------------------------------------------------
837-- Torrent info
838-----------------------------------------------------------------------}
839-- TODO add torrent file validation
840
841-- | Metainfo about particular torrent.
842data Torrent = Torrent
843 { tAnnounce :: !(Maybe URI)
844 -- ^ The URL of the tracker.
845
846 , tAnnounceList :: !(Maybe [[URI]])
847 -- ^ Announce list add multiple tracker support.
848 --
849 -- BEP 12: <http://www.bittorrent.org/beps/bep_0012.html>
850
851 , tComment :: !(Maybe Text)
852 -- ^ Free-form comments of the author.
853
854 , tCreatedBy :: !(Maybe Text)
855 -- ^ Name and version of the program used to create the .torrent.
856
857 , tCreationDate :: !(Maybe POSIXTime)
858 -- ^ Creation time of the torrent, in standard UNIX epoch.
859
860 , tEncoding :: !(Maybe Text)
861 -- ^ String encoding format used to generate the pieces part of
862 -- the info dictionary in the .torrent metafile.
863
864 , tInfoDict :: !InfoDict
865 -- ^ Info about each content file.
866
867 , tNodes :: !(Maybe [NodeAddr HostName])
868 -- ^ This key should be set to the /K closest/ nodes in the
869 -- torrent generating client's routing table. Alternatively, the
870 -- key could be set to a known good 'Network.Address.Node'
871 -- such as one operated by the person generating the torrent.
872 --
873 -- Please do not automatically add \"router.bittorrent.com\" to
874 -- this list because different bittorrent software may prefer to
875 -- use different bootstrap node.
876
877 , tPublisher :: !(Maybe URI)
878 -- ^ Containing the RSA public key of the publisher of the
879 -- torrent. Private counterpart of this key that has the
880 -- authority to allow new peers onto the swarm.
881
882 , tPublisherURL :: !(Maybe URI)
883 , tSignature :: !(Maybe BS.ByteString)
884 -- ^ The RSA signature of the info dictionary (specifically, the
885 -- encrypted SHA-1 hash of the info dictionary).
886 } deriving (Show, Eq, Typeable)
887
888makeLensesFor
889 [ ("tAnnounce" , "announce" )
890 , ("tAnnounceList", "announceList")
891 , ("tComment" , "comment" )
892 , ("tCreatedBy" , "createdBy" )
893 , ("tCreationDate", "creationDate")
894 , ("tEncoding" , "encoding" )
895 , ("tInfoDict" , "infoDict" )
896 , ("tPublisher" , "publisher" )
897 , ("tPublisherURL", "publisherURL")
898 , ("tSignature" , "signature" )
899 ]
900 ''Torrent
901
902instance NFData Torrent where
903 rnf Torrent {..} = rnf tInfoDict
904
905#ifdef VERSION_bencoding
906-- TODO move to bencoding
907instance BEncode URI where
908 toBEncode uri = toBEncode (BC.pack (uriToString id uri ""))
909 {-# INLINE toBEncode #-}
910
911 fromBEncode (BString s) | Just url <- parseURI (BC.unpack s) = return url
912 fromBEncode b = decodingError $ "url <" ++ show b ++ ">"
913 {-# INLINE fromBEncode #-}
914
915--pico2uni :: Pico -> Uni
916--pico2uni = undefined
917
918-- TODO move to bencoding
919instance BEncode POSIXTime where
920 toBEncode pt = toBEncode (floor pt :: Integer)
921 fromBEncode (BInteger i) = return $ fromIntegral i
922 fromBEncode _ = decodingError $ "POSIXTime"
923
924-- TODO to bencoding package
925instance {-# OVERLAPPING #-} BEncode String where
926 toBEncode = toBEncode . T.pack
927 fromBEncode v = T.unpack <$> fromBEncode v
928
929instance BEncode Torrent where
930 toBEncode Torrent {..} = toDict $
931 "announce" .=? tAnnounce
932 .: "announce-list" .=? tAnnounceList
933 .: "comment" .=? tComment
934 .: "created by" .=? tCreatedBy
935 .: "creation date" .=? tCreationDate
936 .: "encoding" .=? tEncoding
937 .: "info" .=! tInfoDict
938 .: "nodes" .=? tNodes
939 .: "publisher" .=? tPublisher
940 .: "publisher-url" .=? tPublisherURL
941 .: "signature" .=? tSignature
942 .: endDict
943
944 fromBEncode = fromDict $ do
945 Torrent <$>? "announce"
946 <*>? "announce-list"
947 <*>? "comment"
948 <*>? "created by"
949 <*>? "creation date"
950 <*>? "encoding"
951 <*>! "info"
952 <*>? "nodes"
953 <*>? "publisher"
954 <*>? "publisher-url"
955 <*>? "signature"
956#endif
957
958(<:>) :: Doc -> Doc -> Doc
959name <:> v = name <> ":" <+> v
960
961(<:>?) :: Doc -> Maybe Doc -> Doc
962_ <:>? Nothing = PP.empty
963name <:>? (Just d) = name <:> d
964
965instance Pretty Torrent where
966 pPrint Torrent {..} =
967 "InfoHash: " <> pPrint (idInfoHash tInfoDict)
968 $$ hang "General" 4 generalInfo
969 $$ hang "Tracker" 4 trackers
970 $$ pPrint tInfoDict
971 where
972 trackers = case tAnnounceList of
973 Nothing -> text (show tAnnounce)
974 Just xxs -> vcat $ L.map ppTier $ L.zip [1..] xxs
975 where
976 ppTier (n, xs) = "Tier #" <> int n <:> vcat (L.map (text . show) xs)
977
978 generalInfo =
979 "Comment" <:>? ((text . T.unpack) <$> tComment) $$
980 "Created by" <:>? ((text . T.unpack) <$> tCreatedBy) $$
981 "Created on" <:>? ((text . show . posixSecondsToUTCTime)
982 <$> tCreationDate) $$
983 "Encoding" <:>? ((text . T.unpack) <$> tEncoding) $$
984 "Publisher" <:>? ((text . show) <$> tPublisher) $$
985 "Publisher URL" <:>? ((text . show) <$> tPublisherURL) $$
986 "Signature" <:>? ((text . show) <$> tSignature)
987
988#ifdef VERSION_bencoding
989-- | No files, no trackers, no nodes, etc...
990instance Default Torrent where
991 def = nullTorrent def
992#endif
993
994-- | A simple torrent contains only required fields.
995nullTorrent :: InfoDict -> Torrent
996nullTorrent info = Torrent
997 Nothing Nothing Nothing Nothing Nothing Nothing
998 info Nothing Nothing Nothing Nothing
999
1000-- | Mime type of torrent files.
1001typeTorrent :: BS.ByteString
1002typeTorrent = "application/x-bittorrent"
1003
1004-- | Extension usually used for torrent files.
1005torrentExt :: String
1006torrentExt = "torrent"
1007
1008-- | Test if this path has proper extension.
1009isTorrentPath :: FilePath -> Bool
1010isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt
1011
1012#ifdef VERSION_bencoding
1013-- | Read and decode a .torrent file.
1014fromFile :: FilePath -> IO Torrent
1015fromFile filepath = do
1016 contents <- BS.readFile filepath
1017 case BE.decode contents of
1018 Right !t -> return t
1019 Left msg -> throwIO $ userError $ msg ++ " while reading torrent file"
1020
1021-- | Encode and write a .torrent file.
1022toFile :: FilePath -> Torrent -> IO ()
1023toFile filepath = BL.writeFile filepath . BE.encode
1024#endif
1025
1026{-----------------------------------------------------------------------
1027-- URN
1028-----------------------------------------------------------------------}
1029
1030-- | Namespace identifier determines the syntactic interpretation of
1031-- namespace-specific string.
1032type NamespaceId = [Text]
1033
1034-- | BitTorrent Info Hash (hence the name) namespace
1035-- identifier. Namespace-specific string /should/ be a base16\/base32
1036-- encoded SHA1 hash of the corresponding torrent /info/ dictionary.
1037--
1038btih :: NamespaceId
1039btih = ["btih"]
1040
1041-- | URN is pesistent location-independent identifier for
1042-- resources. In particular, URNs are used represent torrent names
1043-- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for
1044-- more info.
1045--
1046data URN = URN
1047 { urnNamespace :: NamespaceId -- ^ a namespace identifier;
1048 , urnString :: Text -- ^ a corresponding
1049 -- namespace-specific string.
1050 } deriving (Eq, Ord, Typeable)
1051
1052-----------------------------------------------------------------------
1053
1054instance Convertible URN InfoHash where
1055 safeConvert u @ URN {..}
1056 | urnNamespace /= btih = convError "invalid namespace" u
1057 | otherwise = safeConvert urnString
1058
1059-- | Make resource name for torrent with corresponding
1060-- infohash. Infohash is base16 (hex) encoded.
1061--
1062infohashURN :: InfoHash -> URN
1063infohashURN = URN btih . longHex
1064
1065-- | Meaningless placeholder value.
1066instance Default URN where
1067 def = infohashURN def
1068
1069------------------------------------------------------------------------
1070
1071-- | Render URN to its text representation.
1072renderURN :: URN -> Text
1073renderURN URN {..}
1074 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
1075
1076instance Pretty URN where
1077 pPrint = text . T.unpack . renderURN
1078
1079instance Show URN where
1080 showsPrec n = showsPrec n . T.unpack . renderURN
1081
1082instance QueryValueLike URN where
1083 toQueryValue = toQueryValue . renderURN
1084 {-# INLINE toQueryValue #-}
1085
1086-----------------------------------------------------------------------
1087
1088_unsnoc :: [a] -> Maybe ([a], a)
1089_unsnoc [] = Nothing
1090_unsnoc xs = Just (L.init xs, L.last xs)
1091
1092instance Convertible Text URN where
1093 safeConvert t = case T.split (== ':') t of
1094 uriScheme : body
1095 | T.toLower uriScheme == "urn" ->
1096 case _unsnoc body of
1097 Just (namespace, val) -> pure URN
1098 { urnNamespace = namespace
1099 , urnString = val
1100 }
1101 Nothing -> convError "missing URN string" body
1102 | otherwise -> convError "invalid URN scheme" uriScheme
1103 [] -> convError "missing URN scheme" t
1104
1105instance IsString URN where
1106 fromString = either (error . prettyConvertError) id
1107 . safeConvert . T.pack
1108
1109-- | Try to parse an URN from its text representation.
1110--
1111-- Use 'safeConvert' for detailed error messages.
1112--
1113parseURN :: Text -> Maybe URN
1114parseURN = either (const Nothing) pure . safeConvert
1115
1116{-----------------------------------------------------------------------
1117-- Magnet
1118-----------------------------------------------------------------------}
1119-- $magnet-link
1120--
1121-- Magnet URI scheme is an standard defining Magnet links. Magnet
1122-- links are refer to resources by hash, in particular magnet links
1123-- can refer to torrent using corresponding infohash. In this way,
1124-- magnet links can be used instead of torrent files.
1125--
1126-- This module provides bittorrent specific implementation of magnet
1127-- links.
1128--
1129-- For generic magnet uri scheme see:
1130-- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>,
1131-- <http://www.iana.org/assignments/uri-schemes/prov/magnet>
1132--
1133-- Bittorrent specific details:
1134-- <http://www.bittorrent.org/beps/bep_0009.html>
1135--
1136
1137-- TODO multiple exact topics
1138-- TODO render/parse supplement for URI/query
1139
1140-- | An URI used to identify torrent.
1141data Magnet = Magnet
1142 { -- | Torrent infohash hash. Can be used in DHT queries if no
1143 -- 'tracker' provided.
1144 exactTopic :: !InfoHash -- TODO InfoHash -> URN?
1145
1146 -- | A filename for the file to download. Can be used to
1147 -- display name while waiting for metadata.
1148 , displayName :: Maybe Text
1149
1150 -- | Size of the resource in bytes.
1151 , exactLength :: Maybe Integer
1152
1153 -- | URI pointing to manifest, e.g. a list of further items.
1154 , manifest :: Maybe Text
1155
1156 -- | Search string.
1157 , keywordTopic :: Maybe Text
1158
1159 -- | A source to be queried after not being able to find and
1160 -- download the file in the bittorrent network in a defined
1161 -- amount of time.
1162 , acceptableSource :: Maybe URI
1163
1164 -- | Direct link to the resource.
1165 , exactSource :: Maybe URI
1166
1167 -- | URI to the tracker.
1168 , tracker :: Maybe URI
1169
1170 -- | Additional or experimental parameters.
1171 , supplement :: Map Text Text
1172 } deriving (Eq, Ord, Typeable)
1173
1174instance QueryValueLike Integer where
1175 toQueryValue = toQueryValue . show
1176
1177instance QueryValueLike URI where
1178 toQueryValue = toQueryValue . show
1179
1180instance QueryLike Magnet where
1181 toQuery Magnet {..} =
1182 [ ("xt", toQueryValue $ infohashURN exactTopic)
1183 , ("dn", toQueryValue displayName)
1184 , ("xl", toQueryValue exactLength)
1185 , ("mt", toQueryValue manifest)
1186 , ("kt", toQueryValue keywordTopic)
1187 , ("as", toQueryValue acceptableSource)
1188 , ("xs", toQueryValue exactSource)
1189 , ("tr", toQueryValue tracker)
1190 ]
1191
1192instance QueryValueLike Magnet where
1193 toQueryValue = toQueryValue . renderMagnet
1194
1195instance Convertible QueryText Magnet where
1196 safeConvert xs = do
1197 urnStr <- getTextMsg "xt" "exact topic not defined" xs
1198 infoHash <- convertVia (error "safeConvert" :: URN) urnStr
1199 return Magnet
1200 { exactTopic = infoHash
1201 , displayName = getText "dn" xs
1202 , exactLength = getText "xl" xs >>= getInt
1203 , manifest = getText "mt" xs
1204 , keywordTopic = getText "kt" xs
1205 , acceptableSource = getText "as" xs >>= getURI
1206 , exactSource = getText "xs" xs >>= getURI
1207 , tracker = getText "tr" xs >>= getURI
1208 , supplement = M.empty
1209 }
1210 where
1211 getInt = either (const Nothing) (Just . fst) . signed decimal
1212 getURI = parseURI . T.unpack
1213 getText p = join . L.lookup p
1214 getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps
1215
1216magnetScheme :: URI
1217magnetScheme = URI
1218 { uriScheme = "magnet:"
1219 , uriAuthority = Nothing
1220 , uriPath = ""
1221 , uriQuery = ""
1222 , uriFragment = ""
1223 }
1224
1225isMagnetURI :: URI -> Bool
1226isMagnetURI u = u { uriQuery = "" } == magnetScheme
1227
1228-- | Can be used instead of 'parseMagnet'.
1229instance Convertible URI Magnet where
1230 safeConvert u @ URI {..}
1231 | not (isMagnetURI u) = convError "this is not a magnet link" u
1232 | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery
1233
1234-- | Can be used instead of 'renderMagnet'.
1235instance Convertible Magnet URI where
1236 safeConvert m = pure $ magnetScheme
1237 { uriQuery = BC.unpack $ renderQuery True $ toQuery m }
1238
1239instance Convertible String Magnet where
1240 safeConvert str
1241 | Just uri <- parseURI str = safeConvert uri
1242 | otherwise = convError "unable to parse uri" str
1243
1244------------------------------------------------------------------------
1245
1246-- | Meaningless placeholder value.
1247instance Default Magnet where
1248 def = Magnet
1249 { exactTopic = def
1250 , displayName = Nothing
1251 , exactLength = Nothing
1252 , manifest = Nothing
1253 , keywordTopic = Nothing
1254 , acceptableSource = Nothing
1255 , exactSource = Nothing
1256 , tracker = Nothing
1257 , supplement = M.empty
1258 }
1259
1260-- | Set 'exactTopic' ('xt' param) only, other params are empty.
1261nullMagnet :: InfoHash -> Magnet
1262nullMagnet u = Magnet
1263 { exactTopic = u
1264 , displayName = Nothing
1265 , exactLength = Nothing
1266 , manifest = Nothing
1267 , keywordTopic = Nothing
1268 , acceptableSource = Nothing
1269 , exactSource = Nothing
1270 , tracker = Nothing
1271 , supplement = M.empty
1272 }
1273
1274-- | Like 'nullMagnet' but also include 'displayName' ('dn' param).
1275simpleMagnet :: Torrent -> Magnet
1276simpleMagnet Torrent {tInfoDict = InfoDict {..}}
1277 = (nullMagnet idInfoHash)
1278 { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo
1279 }
1280
1281-- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and
1282-- 'tracker' ('tr' param).
1283--
1284detailedMagnet :: Torrent -> Magnet
1285detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce}
1286 = (simpleMagnet t)
1287 { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo
1288 , tracker = tAnnounce
1289 }
1290
1291-----------------------------------------------------------------------
1292
1293parseMagnetStr :: String -> Maybe Magnet
1294parseMagnetStr = either (const Nothing) Just . safeConvert
1295
1296renderMagnetStr :: Magnet -> String
1297renderMagnetStr = show . (convert :: Magnet -> URI)
1298
1299instance Pretty Magnet where
1300 pPrint = PP.text . renderMagnetStr
1301
1302instance Show Magnet where
1303 show = renderMagnetStr
1304 {-# INLINE show #-}
1305
1306instance Read Magnet where
1307 readsPrec _ xs
1308 | Just m <- parseMagnetStr mstr = [(m, rest)]
1309 | otherwise = []
1310 where
1311 (mstr, rest) = L.break (== ' ') xs
1312
1313instance IsString Magnet where
1314 fromString str = fromMaybe (error msg) $ parseMagnetStr str
1315 where
1316 msg = "unable to parse magnet: " ++ str
1317
1318-- | Try to parse magnet link from urlencoded string. Use
1319-- 'safeConvert' to find out error location.
1320--
1321parseMagnet :: Text -> Maybe Magnet
1322parseMagnet = parseMagnetStr . T.unpack
1323{-# INLINE parseMagnet #-}
1324
1325-- | Render magnet link to urlencoded string
1326renderMagnet :: Magnet -> Text
1327renderMagnet = T.pack . renderMagnetStr
1328{-# INLINE renderMagnet #-}
diff --git a/src/Data/Word64Map.hs b/src/Data/Word64Map.hs
new file mode 100644
index 00000000..9e93c8c8
--- /dev/null
+++ b/src/Data/Word64Map.hs
@@ -0,0 +1,62 @@
1{-# LANGUAGE RankNTypes #-}
2{-# LANGUAGE ScopedTypeVariables #-}
3{-# LANGUAGE UnboxedTuples #-}
4module Data.Word64Map where
5
6import Data.Bits
7import qualified Data.IntMap as IntMap
8 ;import Data.IntMap (IntMap)
9import Data.Typeable
10import Data.Word
11
12-- | Since 'Int' may be 32 or 64 bits, this function is provided as a
13-- convenience to test if an integral type, such as 'Data.Word.Word64', can be
14-- safely transformed into an 'Int' for use with 'IntMap'.
15--
16-- Returns 'True' if the proxied type can be losslessly converted to 'Int' using
17-- 'fromIntegral'.
18fitsInInt :: forall proxy word. (Bounded word, Integral word) => proxy word -> Bool
19fitsInInt proxy = (original == casted)
20 where
21 original = div maxBound 2 :: word
22 casted = fromIntegral (fromIntegral original :: Int) :: word
23
24newtype Word64Map a = Word64Map (IntMap (IntMap a))
25
26empty :: Word64Map a
27empty = Word64Map IntMap.empty
28
29-- Warning: This function assumes an 'Int' is either 64 or 32 bits.
30keyFrom64 :: Word64 -> (# Int,Int #)
31keyFrom64 w8 =
32 if fitsInInt (Proxy :: Proxy Word64)
33 then (# fromIntegral w8 , 0 #)
34 else (# fromIntegral (w8 `shiftR` 32), fromIntegral w8 #)
35{-# INLINE keyFrom64 #-}
36
37lookup :: Word64 -> Word64Map b -> Maybe b
38lookup w8 (Word64Map m) | (# hi,lo #) <- keyFrom64 w8 = do
39 m' <- IntMap.lookup hi m
40 IntMap.lookup lo m'
41{-# INLINE lookup #-}
42
43insert :: Word64 -> b -> Word64Map b -> Word64Map b
44insert w8 b (Word64Map m) | (# hi,lo #) <- keyFrom64 w8
45 = Word64Map $ IntMap.alter (Just . maybe (IntMap.singleton lo b)
46 (IntMap.insert lo b))
47 hi
48 m
49{-# INLINE insert #-}
50
51delete :: Word64 -> Word64Map b -> Word64Map b
52delete w8 (Word64Map m) | (# hi,lo #) <- keyFrom64 w8
53 = Word64Map $ IntMap.alter (maybe Nothing
54 (\m' -> case IntMap.delete lo m' of
55 m'' | IntMap.null m'' -> Nothing
56 m'' -> Just m''))
57 hi
58 m
59{-# INLINE delete #-}
60
61
62
diff --git a/src/Data/Wrapper/PSQ.hs b/src/Data/Wrapper/PSQ.hs
new file mode 100644
index 00000000..631af6ec
--- /dev/null
+++ b/src/Data/Wrapper/PSQ.hs
@@ -0,0 +1,81 @@
1{-# LANGUAGE PatternSynonyms #-}
2{-# LANGUAGE CPP #-}
3{-# LANGUAGE ConstraintKinds #-}
4module Data.Wrapper.PSQ
5#if 0
6 ( module Data.Wrapper.PSQ , module Data.PSQueue ) where
7
8import Data.PSQueue hiding (foldr, foldl)
9import qualified Data.PSQueue as PSQueue
10
11type PSQKey k = (Ord k)
12
13-- | Wrapper over PSQueue-style foldr to implement a psqueues-style interface.
14fold' :: (Ord k, Ord p) => (k -> p -> () -> a -> a) -> a -> PSQ k p -> a
15fold' f a q = PSQueue.foldr f' a q
16 where
17 f' (k :-> prio) x = f k prio () x
18
19#else
20 ( module Data.Wrapper.PSQ , module HashPSQ ) where
21
22import Data.OrdPSQ as OrdPSQ hiding (insert, map, singleton, minView)
23import qualified Data.OrdPSQ as OrdPSQ
24
25import Data.HashPSQ as HashPSQ hiding (insert, map, singleton, minView)
26import qualified Data.HashPSQ as Q
27import Data.Hashable
28
29type PSQ' k p v = HashPSQ k p v
30type PSQ k p = PSQ' k p ()
31
32type Binding' k p v = (k,p,v)
33type Binding k p = Binding' k p ()
34
35type PSQKey k = (Hashable k, Ord k)
36
37pattern (:->) :: k -> p -> Binding k p
38pattern k :-> p <- (k,p,_) where k :-> p = (k,p,())
39
40-- I tried defining (::->) :: (k,v) -> p -> Binding' k p v
41-- but no luck...
42pattern Binding :: k -> v -> p -> Binding' k p v
43pattern Binding k v p <- (k,p,v) where Binding k v p = (k,p,v)
44
45key :: (k,p,v) -> k
46key (k,p,v) = k
47{-# INLINE key #-}
48
49prio :: (k,p,v) -> p
50prio (k,p,v) = p
51{-# INLINE prio #-}
52
53insert :: (PSQKey k, Ord p) => k -> p -> PSQ k p -> PSQ k p
54insert k p q = Q.insert k p () q
55{-# INLINE insert #-}
56
57insert' :: (PSQKey k, Ord p) => k -> v -> p -> PSQ' k p v -> PSQ' k p v
58insert' k v p q = Q.insert k p v q
59{-# INLINE insert' #-}
60
61insertWith :: (PSQKey k, Ord p) => (p -> p -> p) -> k -> p -> PSQ k p -> PSQ k p
62insertWith f k p0 q = snd $ Q.alter f' k q
63 where
64 f' (Just (p,())) = ((),Just (f p0 p, ()))
65 f' Nothing = ((),Just (p0,()))
66{-# INLINE insertWith #-}
67
68singleton :: (PSQKey k, Ord p) => k -> p -> PSQ k p
69singleton k p = Q.singleton k p ()
70{-# INLINE singleton #-}
71
72singleton' :: (PSQKey k, Ord p) => k -> v -> p -> PSQ' k p v
73singleton' k v p = Q.singleton k p v
74{-# INLINE singleton' #-}
75
76
77minView :: (PSQKey k, Ord p) => PSQ' k p v -> Maybe (Binding' k p v, PSQ' k p v)
78minView q = fmap (\(k,p,v,q') -> (Binding k v p, q')) $ Q.minView q
79{-# INLINE minView #-}
80
81#endif
diff --git a/src/Data/Wrapper/PSQInt.hs b/src/Data/Wrapper/PSQInt.hs
new file mode 100644
index 00000000..c61b7ab6
--- /dev/null
+++ b/src/Data/Wrapper/PSQInt.hs
@@ -0,0 +1,55 @@
1{-# LANGUAGE PatternSynonyms #-}
2{-# LANGUAGE CPP #-}
3{-# LANGUAGE ConstraintKinds #-}
4module Data.Wrapper.PSQInt
5#if 0
6 ( module Data.Wrapper.PSQInt , module Data.PSQueue ) where
7
8import Data.PSQueue hiding (foldr, foldl, PSQ)
9import qualified Data.PSQueue as PSQueue
10
11type PSQ p = PSQueue.PSQ Int p
12
13-- | Wrapper over PSQueue-style foldr to implement a psqueues-style interface.
14fold' :: (Ord p) => (Int -> p -> () -> a -> a) -> a -> PSQ p -> a
15fold' f a q = PSQueue.foldr f' a q
16 where
17 f' (k :-> prio) x = f k prio () x
18
19#else
20 ( module Data.Wrapper.PSQInt
21 , module IntPSQ
22 , pattern (:->)
23 , key
24 , prio
25 ) where
26
27import Data.Wrapper.PSQ (Binding, pattern (:->), key, prio)
28
29import Data.IntPSQ as IntPSQ hiding (insert, map, singleton, minView)
30import qualified Data.IntPSQ as Q
31
32type PSQ p = IntPSQ p ()
33
34type PSQKey = ()
35
36insert :: (Ord p) => Int -> p -> PSQ p -> PSQ p
37insert k p q = Q.insert k p () q
38{-# INLINE insert #-}
39
40insertWith :: (Ord p) => (p -> p -> p) -> Int -> p -> PSQ p -> PSQ p
41insertWith f k p0 q = snd $ Q.alter f' k q
42 where
43 f' (Just (p,())) = ((),Just (f p0 p, ()))
44 f' Nothing = ((),Nothing)
45{-# INLINE insertWith #-}
46
47singleton :: (Ord p) => Int -> p -> PSQ p
48singleton k p = Q.singleton k p ()
49{-# INLINE singleton #-}
50
51minView :: (Ord p) => PSQ p -> Maybe (Binding Int p, PSQ p)
52minView q = fmap (\(k,p,(),q') -> (k :-> p, q')) $ Q.minView q
53{-# INLINE minView #-}
54
55#endif
diff --git a/src/Network/Address.hs b/src/Network/Address.hs
new file mode 100644
index 00000000..4f163b79
--- /dev/null
+++ b/src/Network/Address.hs
@@ -0,0 +1,1225 @@
1-- |
2-- Module : Network.Address
3-- Copyright : (c) Sam Truzjan 2013
4-- (c) Daniel Gröber 2013
5-- License : BSD3
6-- Maintainer : pxqr.sta@gmail.com
7-- Stability : provisional
8-- Portability : portable
9--
10-- Peer and Node addresses.
11--
12{-# LANGUAGE CPP #-}
13{-# LANGUAGE FlexibleInstances #-}
14{-# LANGUAGE FlexibleContexts #-}
15{-# LANGUAGE RecordWildCards #-}
16{-# LANGUAGE ScopedTypeVariables #-}
17{-# LANGUAGE StandaloneDeriving #-}
18{-# LANGUAGE ViewPatterns #-}
19{-# LANGUAGE GeneralizedNewtypeDeriving #-}
20{-# LANGUAGE MultiParamTypeClasses #-}
21{-# LANGUAGE DeriveDataTypeable #-}
22{-# LANGUAGE DeriveFunctor #-}
23{-# LANGUAGE DeriveFoldable #-}
24{-# LANGUAGE DeriveTraversable #-}
25{-# LANGUAGE TemplateHaskell #-}
26{-# OPTIONS -fno-warn-orphans #-}
27module Network.Address
28 ( -- * Address
29 Address (..)
30 , fromAddr
31 , PortNumber
32 , SockAddr
33
34 -- ** IP
35 , IPv4
36 , IPv6
37 , IP (..)
38 , un4map
39 , WantIP (..)
40 , ipFamily
41 , is4mapped
42 , either4or6
43
44 -- * PeerId
45 -- $peer-id
46 , PeerId
47
48 -- ** Generation
49 , genPeerId
50 , timestamp
51 , entropy
52
53 -- ** Encoding
54 , azureusStyle
55 , shadowStyle
56 , defaultClientId
57 , defaultVersionNumber
58
59 -- * PeerAddr
60 -- $peer-addr
61 , PeerAddr(..)
62 , defaultPorts
63 , peerSockAddr
64 , peerSocket
65
66 -- * Node
67 , NodeAddr (..)
68
69 -- ** Id
70 , testIdBit
71 , bucketRange
72 , genBucketSample
73 , genBucketSample'
74
75 -- * Fingerprint
76 -- $fingerprint
77 , Software (..)
78 , Fingerprint (..)
79 , libFingerprint
80 , fingerprint
81
82 -- * Utils
83 , libUserAgent
84 , sockAddrPort
85 , setPort
86 , getBindAddress
87 ) where
88
89import Control.Applicative
90import Control.Monad
91import Control.Exception (onException)
92#ifdef VERSION_bencoding
93import Data.BEncode as BE
94import Data.BEncode.BDict (BKey)
95#endif
96import Data.Bits
97import qualified Data.ByteString as BS
98import qualified Data.ByteString.Internal as BS
99import Data.ByteString.Char8 as BC
100import Data.ByteString.Char8 as BS8
101import qualified Data.ByteString.Lazy as BL
102import qualified Data.ByteString.Lazy.Builder as BS
103import Data.Char
104import Data.Convertible
105import Data.Default
106import Data.IP
107import Data.List as L
108import Data.List.Split as L
109import Data.Maybe (fromMaybe, catMaybes, mapMaybe)
110import Data.Monoid
111import Data.Hashable
112import Data.Ord
113import Data.Serialize as S
114import Data.String
115import Data.Time
116import Data.Typeable
117import Data.Version
118import Data.Word
119import qualified Text.ParserCombinators.ReadP as RP
120import Text.Read (readMaybe)
121import Network.HTTP.Types.QueryLike
122import Network.Socket
123import Text.PrettyPrint as PP hiding ((<>))
124import Text.PrettyPrint.HughesPJClass hiding ((<>),($$))
125#if !MIN_VERSION_time(1,5,0)
126import System.Locale (defaultTimeLocale)
127#endif
128import System.Entropy
129import System.IO (stderr)
130
131-- import Paths_bittorrent (version)
132
133instance Pretty UTCTime where
134 pPrint = PP.text . show
135
136setPort :: PortNumber -> SockAddr -> SockAddr
137setPort port (SockAddrInet _ h ) = SockAddrInet port h
138setPort port (SockAddrInet6 _ f h s) = SockAddrInet6 port f h s
139setPort _ addr = addr
140{-# INLINE setPort #-}
141
142-- | Obtains the port associated with a socket address
143-- if one is associated with it.
144sockAddrPort :: SockAddr -> Maybe PortNumber
145sockAddrPort (SockAddrInet p _ ) = Just p
146sockAddrPort (SockAddrInet6 p _ _ _) = Just p
147sockAddrPort _ = Nothing
148{-# INLINE sockAddrPort #-}
149
150class (Eq a, Serialize a, Typeable a, Hashable a, Pretty a)
151 => Address a where
152 toSockAddr :: a -> SockAddr
153 fromSockAddr :: SockAddr -> Maybe a
154
155fromAddr :: (Address a, Address b) => a -> Maybe b
156fromAddr = fromSockAddr . toSockAddr
157
158-- | Note that port is zeroed.
159instance Address IPv4 where
160 toSockAddr = SockAddrInet 0 . toHostAddress
161 fromSockAddr (SockAddrInet _ h) = Just (fromHostAddress h)
162 fromSockAddr _ = Nothing
163
164-- | Note that port is zeroed.
165instance Address IPv6 where
166 toSockAddr h = SockAddrInet6 0 0 (toHostAddress6 h) 0
167 fromSockAddr (SockAddrInet6 _ _ h _) = Just (fromHostAddress6 h)
168 fromSockAddr _ = Nothing
169
170-- | Note that port is zeroed.
171instance Address IP where
172 toSockAddr (IPv4 h) = toSockAddr h
173 toSockAddr (IPv6 h) = toSockAddr h
174 fromSockAddr sa =
175 IPv4 <$> fromSockAddr sa
176 <|> IPv6 <$> fromSockAddr sa
177
178data NodeAddr a = NodeAddr
179 { nodeHost :: !a
180 , nodePort :: {-# UNPACK #-} !PortNumber
181 } deriving (Eq, Ord, Typeable, Functor, Foldable, Traversable)
182
183instance Show a => Show (NodeAddr a) where
184 showsPrec i NodeAddr {..}
185 = showsPrec i nodeHost <> showString ":" <> showsPrec i nodePort
186
187instance Read (NodeAddr IPv4) where
188 readsPrec i = RP.readP_to_S $ do
189 ipv4 <- RP.readS_to_P (readsPrec i)
190 _ <- RP.char ':'
191 port <- toEnum <$> RP.readS_to_P (readsPrec i)
192 return $ NodeAddr ipv4 port
193
194-- | @127.0.0.1:6882@
195instance Default (NodeAddr IPv4) where
196 def = "127.0.0.1:6882"
197
198-- | KRPC compatible encoding.
199instance Serialize a => Serialize (NodeAddr a) where
200 get = NodeAddr <$> get <*> get
201 {-# INLINE get #-}
202 put NodeAddr {..} = put nodeHost >> put nodePort
203 {-# INLINE put #-}
204
205-- | Example:
206--
207-- @nodePort \"127.0.0.1:6881\" == 6881@
208--
209instance IsString (NodeAddr IPv4) where
210 fromString str
211 | [hostAddrStr, portStr] <- splitWhen (== ':') str
212 , Just hostAddr <- readMaybe hostAddrStr
213 , Just portNum <- toEnum <$> readMaybe portStr
214 = NodeAddr hostAddr portNum
215 | otherwise = error $ "fromString: unable to parse (NodeAddr IPv4): " ++ str
216
217
218instance Hashable a => Hashable (NodeAddr a) where
219 hashWithSalt s NodeAddr {..} = hashWithSalt s (nodeHost, nodePort)
220 {-# INLINE hashWithSalt #-}
221
222instance Pretty ip => Pretty (NodeAddr ip) where
223 pPrint NodeAddr {..} = pPrint nodeHost <> ":" <> pPrint nodePort
224
225
226
227instance Address PeerAddr where
228 toSockAddr PeerAddr {..} = setPort peerPort $ toSockAddr peerHost
229 fromSockAddr sa = PeerAddr Nothing <$> fromSockAddr sa <*> sockAddrPort sa
230
231{-----------------------------------------------------------------------
232-- Peer id
233-----------------------------------------------------------------------}
234-- $peer-id
235--
236-- 'PeerID' represent self assigned peer identificator. Ideally each
237-- host in the network should have unique peer id to avoid
238-- collisions, therefore for peer ID generation we use good entropy
239-- source. Peer ID is sent in /tracker request/, sent and received in
240-- /peer handshakes/ and used in DHT queries.
241--
242
243-- TODO use unpacked Word160 form (length is known statically)
244
245-- | Peer identifier is exactly 20 bytes long bytestring.
246newtype PeerId = PeerId { getPeerId :: ByteString }
247 deriving ( Show, Eq, Ord, Typeable
248#ifdef VERSION_bencoding
249 , BEncode
250#endif
251 )
252
253peerIdLen :: Int
254peerIdLen = 20
255
256-- | For testing purposes only.
257instance Default PeerId where
258 def = azureusStyle defaultClientId defaultVersionNumber ""
259
260instance Hashable PeerId where
261 hashWithSalt = hashUsing getPeerId
262 {-# INLINE hashWithSalt #-}
263
264instance Serialize PeerId where
265 put = putByteString . getPeerId
266 get = PeerId <$> getBytes peerIdLen
267
268instance QueryValueLike PeerId where
269 toQueryValue (PeerId pid) = Just pid
270 {-# INLINE toQueryValue #-}
271
272instance IsString PeerId where
273 fromString str
274 | BS.length bs == peerIdLen = PeerId bs
275 | otherwise = error $ "Peer id should be 20 bytes long: " ++ show str
276 where
277 bs = fromString str
278
279instance Pretty PeerId where
280 pPrint = text . BC.unpack . getPeerId
281
282instance Convertible BS.ByteString PeerId where
283 safeConvert bs
284 | BS.length bs == peerIdLen = pure (PeerId bs)
285 | otherwise = convError "invalid length" bs
286
287------------------------------------------------------------------------
288
289-- | Pad bytestring so it's becomes exactly request length. Conversion
290-- is done like so:
291--
292-- * length < size: Complete bytestring by given charaters.
293--
294-- * length = size: Output bytestring as is.
295--
296-- * length > size: Drop last (length - size) charaters from a
297-- given bytestring.
298--
299byteStringPadded :: ByteString -- ^ bytestring to be padded.
300 -> Int -- ^ size of result builder.
301 -> Char -- ^ character used for padding.
302 -> BS.Builder
303byteStringPadded bs s c =
304 BS.byteString (BS.take s bs) <>
305 BS.byteString (BC.replicate padLen c)
306 where
307 padLen = s - min (BS.length bs) s
308
309-- | Azureus-style encoding have the following layout:
310--
311-- * 1 byte : '-'
312--
313-- * 2 bytes: client id
314--
315-- * 4 bytes: version number
316--
317-- * 1 byte : '-'
318--
319-- * 12 bytes: random number
320--
321azureusStyle :: ByteString -- ^ 2 character client ID, padded with 'H'.
322 -> ByteString -- ^ Version number, padded with 'X'.
323 -> ByteString -- ^ Random number, padded with '0'.
324 -> PeerId -- ^ Azureus-style encoded peer ID.
325azureusStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
326 BS.char8 '-' <>
327 byteStringPadded cid 2 'H' <>
328 byteStringPadded ver 4 'X' <>
329 BS.char8 '-' <>
330 byteStringPadded rnd 12 '0'
331
332-- | Shadow-style encoding have the following layout:
333--
334-- * 1 byte : client id.
335--
336-- * 0-4 bytes: version number. If less than 4 then padded with
337-- '-' char.
338--
339-- * 15 bytes : random number. If length is less than 15 then
340-- padded with '0' char.
341--
342shadowStyle :: Char -- ^ Client ID.
343 -> ByteString -- ^ Version number.
344 -> ByteString -- ^ Random number.
345 -> PeerId -- ^ Shadow style encoded peer ID.
346shadowStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
347 BS.char8 cid <>
348 byteStringPadded ver 4 '-' <>
349 byteStringPadded rnd 15 '0'
350
351
352-- | 'HS'- 2 bytes long client identifier.
353defaultClientId :: ByteString
354defaultClientId = "HS"
355
356-- | Gives exactly 4 bytes long version number for any version of the
357-- package. Version is taken from .cabal file.
358defaultVersionNumber :: ByteString
359defaultVersionNumber = BS.take 4 $ BC.pack $ foldMap show $
360 versionBranch myVersion
361 where
362 Fingerprint _ myVersion = libFingerprint
363
364------------------------------------------------------------------------
365
366-- | Gives 15 characters long decimal timestamp such that:
367--
368-- * 6 bytes : first 6 characters from picoseconds obtained with %q.
369--
370-- * 1 byte : character \'.\' for readability.
371--
372-- * 9..* bytes: number of whole seconds since the Unix epoch
373-- (!)REVERSED.
374--
375-- Can be used both with shadow and azureus style encoding. This
376-- format is used to make the ID's readable for debugging purposes.
377--
378timestamp :: IO ByteString
379timestamp = (BC.pack . format) <$> getCurrentTime
380 where
381 format t = L.take 6 (formatTime defaultTimeLocale "%q" t) ++ "." ++
382 L.take 9 (L.reverse (formatTime defaultTimeLocale "%s" t))
383
384-- | Gives 15 character long random bytestring. This is more robust
385-- method for generation of random part of peer ID than 'timestamp'.
386entropy :: IO ByteString
387entropy = getEntropy 15
388
389-- NOTE: entropy generates incorrrect peer id
390
391-- | Here we use 'azureusStyle' encoding with the following args:
392--
393-- * 'HS' for the client id; ('defaultClientId')
394--
395-- * Version of the package for the version number;
396-- ('defaultVersionNumber')
397--
398-- * UTC time day ++ day time for the random number. ('timestamp')
399--
400genPeerId :: IO PeerId
401genPeerId = azureusStyle defaultClientId defaultVersionNumber <$> timestamp
402
403{-----------------------------------------------------------------------
404-- Peer Addr
405-----------------------------------------------------------------------}
406-- $peer-addr
407--
408-- 'PeerAddr' is used to represent peer address. Currently it's
409-- just peer IP and peer port but this might change in future.
410--
411
412{-----------------------------------------------------------------------
413-- Port number
414-----------------------------------------------------------------------}
415
416#ifdef VERSION_bencoding
417instance BEncode PortNumber where
418 toBEncode = toBEncode . fromEnum
419 fromBEncode = fromBEncode >=> portNumber
420 where
421 portNumber :: Integer -> BE.Result PortNumber
422 portNumber n
423 | 0 <= n && n <= fromIntegral (maxBound :: Word16)
424 = pure $ fromIntegral n
425 | otherwise = decodingError $ "PortNumber: " ++ show n
426#endif
427{-----------------------------------------------------------------------
428-- IP addr
429-----------------------------------------------------------------------}
430
431class IPAddress i where
432 toHostAddr :: i -> Either HostAddress HostAddress6
433
434instance IPAddress IPv4 where
435 toHostAddr = Left . toHostAddress
436 {-# INLINE toHostAddr #-}
437
438instance IPAddress IPv6 where
439 toHostAddr = Right . toHostAddress6
440 {-# INLINE toHostAddr #-}
441
442instance IPAddress IP where
443 toHostAddr (IPv4 ip) = toHostAddr ip
444 toHostAddr (IPv6 ip) = toHostAddr ip
445 {-# INLINE toHostAddr #-}
446
447deriving instance Typeable IP
448deriving instance Typeable IPv4
449deriving instance Typeable IPv6
450
451#ifdef VERSION_bencoding
452ipToBEncode :: Show i => i -> BValue
453ipToBEncode ip = BString $ BS8.pack $ show ip
454{-# INLINE ipToBEncode #-}
455
456ipFromBEncode :: Read a => BValue -> BE.Result a
457ipFromBEncode (BString (BS8.unpack -> ipStr))
458 | Just ip <- readMaybe (ipStr) = pure ip
459 | otherwise = decodingError $ "IP: " ++ ipStr
460ipFromBEncode _ = decodingError $ "IP: addr should be a bstring"
461
462instance BEncode IP where
463 toBEncode = ipToBEncode
464 {-# INLINE toBEncode #-}
465 fromBEncode = ipFromBEncode
466 {-# INLINE fromBEncode #-}
467
468instance BEncode IPv4 where
469 toBEncode = ipToBEncode
470 {-# INLINE toBEncode #-}
471 fromBEncode = ipFromBEncode
472 {-# INLINE fromBEncode #-}
473
474instance BEncode IPv6 where
475 toBEncode = ipToBEncode
476 {-# INLINE toBEncode #-}
477 fromBEncode = ipFromBEncode
478 {-# INLINE fromBEncode #-}
479#endif
480
481-- | Peer address info normally extracted from peer list or peer
482-- compact list encoding.
483data PeerAddr = PeerAddr
484 { peerId :: !(Maybe PeerId)
485
486 -- | This is usually 'IPv4', 'IPv6', 'IP' or unresolved
487 -- 'HostName'.
488 , peerHost :: !IP
489
490 -- | The port the peer listenning for incoming P2P sessions.
491 , peerPort :: {-# UNPACK #-} !PortNumber
492 } deriving (Show, Eq, Ord, Typeable)
493
494#ifdef VERSION_bencoding
495peer_ip_key, peer_id_key, peer_port_key :: BKey
496peer_ip_key = "ip"
497peer_id_key = "peer id"
498peer_port_key = "port"
499
500-- | The tracker's 'announce response' compatible encoding.
501instance BEncode PeerAddr where
502 toBEncode PeerAddr {..} = toDict $
503 peer_ip_key .=! peerHost
504 .: peer_id_key .=? peerId
505 .: peer_port_key .=! peerPort
506 .: endDict
507
508 fromBEncode = fromDict $ do
509 peerAddr <$>! peer_ip_key
510 <*>? peer_id_key
511 <*>! peer_port_key
512 where
513 peerAddr = flip PeerAddr
514#endif
515
516-- | The tracker's 'compact peer list' compatible encoding. The
517-- 'peerId' is always 'Nothing'.
518--
519-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
520--
521-- WARNING: Input must be exactly 6 or 18 bytes so that we can identify IP version.
522--
523instance Serialize PeerAddr where
524 put PeerAddr {..} = put peerHost >> put peerPort
525 get = do
526 cnt <- remaining
527 PeerAddr Nothing <$> isolate (cnt - 2) get <*> get
528
529-- | @127.0.0.1:6881@
530instance Default PeerAddr where
531 def = "127.0.0.1:6881"
532
533-- | Example:
534--
535-- @peerPort \"127.0.0.1:6881\" == 6881@
536--
537instance IsString PeerAddr where
538 fromString str
539 | [hostAddrStr, portStr] <- splitWhen (== ':') str
540 , Just hostAddr <- readMaybe hostAddrStr
541 , Just portNum <- toEnum <$> readMaybe portStr
542 = PeerAddr Nothing (IPv4 hostAddr) portNum
543 | [((ip,port),"")] <- readsIPv6_port str =
544 PeerAddr Nothing (IPv6 ip) port
545 | otherwise = error $ "fromString: unable to parse IP: " ++ str
546
547instance Read PeerAddr where
548 readsPrec i = RP.readP_to_S $ do
549 ip <- IPv4 <$> ( RP.readS_to_P (readsPrec i) )
550 <|> IPv6 <$> ( RP.char '[' *> (RP.readS_to_P reads) <* RP.char ']' )
551 _ <- RP.char ':'
552 port <- toEnum <$> RP.readS_to_P (readsPrec i)
553 return $ PeerAddr Nothing ip port
554
555readsIPv6_port :: String -> [((IPv6, PortNumber), String)]
556readsIPv6_port = RP.readP_to_S $ do
557 ip <- RP.char '[' *> (RP.readS_to_P reads) <* RP.char ']'
558 _ <- RP.char ':'
559 port <- toEnum <$> read <$> (RP.many1 $ RP.satisfy isDigit) <* RP.eof
560 return (ip,port)
561
562
563-- | fingerprint + "at" + dotted.host.inet.addr:port
564instance Pretty PeerAddr where
565 pPrint PeerAddr {..}
566 | Just pid <- peerId = pPrint (fingerprint pid) <+> "at" <+> paddr
567 | otherwise = paddr
568 where
569 paddr = pPrint peerHost <> ":" <> text (show peerPort)
570
571instance Hashable PeerAddr where
572 hashWithSalt s PeerAddr {..} =
573 s `hashWithSalt` peerId `hashWithSalt` peerHost `hashWithSalt` peerPort
574
575-- | Ports typically reserved for bittorrent P2P listener.
576defaultPorts :: [PortNumber]
577defaultPorts = [6881..6889]
578
579_peerSockAddr :: PeerAddr -> (Family, SockAddr)
580_peerSockAddr PeerAddr {..} =
581 case peerHost of
582 IPv4 ipv4 ->
583 (AF_INET, SockAddrInet peerPort (toHostAddress ipv4))
584 IPv6 ipv6 ->
585 (AF_INET6, SockAddrInet6 peerPort 0 (toHostAddress6 ipv6) 0)
586
587peerSockAddr :: PeerAddr -> SockAddr
588peerSockAddr = snd . _peerSockAddr
589
590-- | Create a socket connected to the address specified in a peerAddr
591peerSocket :: SocketType -> PeerAddr -> IO Socket
592peerSocket socketType pa = do
593 let (family, addr) = _peerSockAddr pa
594 sock <- socket family socketType defaultProtocol
595 connect sock addr
596 return sock
597
598{-----------------------------------------------------------------------
599-- Node info
600-----------------------------------------------------------------------}
601-- $node-info
602--
603-- A \"node\" is a client\/server listening on a UDP port
604-- implementing the distributed hash table protocol. The DHT is
605-- composed of nodes and stores the location of peers. BitTorrent
606-- clients include a DHT node, which is used to contact other nodes
607-- in the DHT to get the location of peers to download from using
608-- the BitTorrent protocol.
609
610-- asNodeId :: ByteString -> NodeId
611-- asNodeId bs = NodeId $ BS.take nodeIdSize bs
612
613{-
614
615-- | Test if the nth bit is set.
616testIdBit :: NodeId -> Word -> Bool
617testIdBit (NodeId bs) i
618 | fromIntegral i < nodeIdSize * 8
619 , (q, r) <- quotRem (fromIntegral i) 8
620 = testBit (BS.index bs q) (7 - r)
621 | otherwise = False
622-}
623
624testIdBit :: FiniteBits bs => bs -> Word -> Bool
625testIdBit bs i = testBit bs (fromIntegral (finiteBitSize bs - 1 - fromIntegral i))
626{-# INLINE testIdBit #-}
627
628-- | Generate a random 'NodeId' within a range suitable for a bucket. To
629-- obtain a sample for bucket number /index/ where /is_last/ indicates if this
630-- is for the current deepest bucket in our routing table:
631--
632-- > sample <- genBucketSample nid (bucketRange index is_last)
633genBucketSample :: ( FiniteBits nid
634 , Serialize nid
635 ) => nid -> (Int,Word8,Word8) -> IO nid
636genBucketSample n qmb = genBucketSample' getEntropy n qmb
637
638-- | Generalizion of 'genBucketSample' that accepts a byte generator
639-- function to use instead of the system entropy.
640genBucketSample' :: forall m dht nid.
641 ( Applicative m
642 , FiniteBits nid
643 , Serialize nid
644 ) =>
645 (Int -> m ByteString) -> nid -> (Int,Word8,Word8) -> m nid
646genBucketSample' gen self (q,m,b)
647 | q <= 0 = either error id . S.decode <$> gen nodeIdSize
648 | q >= nodeIdSize = pure self
649 | otherwise = either error id . S.decode . build <$> gen (nodeIdSize - q + 1)
650 where
651 nodeIdSize = finiteBitSize (undefined :: nid) `div` 8
652
653 -- Prepends q bytes to modified input:
654 -- applies mask m
655 -- toggles bit b
656 build tl = BS.init hd <> BS.cons (h .|. t) (BS.tail tl)
657 where
658 hd = BS.take q $ S.encode self
659 h = xor b (complement m .&. BS.last hd)
660 t = m .&. BS.head tl
661
662
663------------------------------------------------------------------------
664
665-- | Accepts a depth/index of a bucket and whether or not it is the last one,
666-- yields:
667--
668-- count of leading bytes to be copied from your node id.
669--
670-- mask to clear the extra bits of the last copied byte
671--
672-- mask to toggle the last copied bit if it is not the last bucket
673--
674-- Normally this is used with 'genBucketSample' to obtain a random id suitable
675-- for refreshing a particular bucket.
676bucketRange :: Int -> Bool -> (Int, Word8, Word8)
677bucketRange depth is_last = (q,m,b)
678 where
679 (q,r) = divMod ((if is_last then (+7) else (+8)) depth) 8
680 m = 2^(7-r) - 1
681 b = if is_last then 0 else 2^(7-r)
682
683------------------------------------------------------------------------
684
685#ifdef VERSION_bencoding
686-- | Torrent file compatible encoding.
687instance BEncode a => BEncode (NodeAddr a) where
688 toBEncode NodeAddr {..} = toBEncode (nodeHost, nodePort)
689 {-# INLINE toBEncode #-}
690 fromBEncode b = uncurry NodeAddr <$> fromBEncode b
691 {-# INLINE fromBEncode #-}
692#endif
693
694
695instance Hashable PortNumber where
696 hashWithSalt s = hashWithSalt s . fromEnum
697 {-# INLINE hashWithSalt #-}
698
699instance Pretty PortNumber where
700 pPrint = PP.int . fromEnum
701 {-# INLINE pPrint #-}
702
703instance Serialize PortNumber where
704 get = fromIntegral <$> getWord16be
705 {-# INLINE get #-}
706 put = putWord16be . fromIntegral
707 {-# INLINE put #-}
708
709instance Pretty IPv4 where
710 pPrint = PP.text . show
711 {-# INLINE pPrint #-}
712
713instance Pretty IPv6 where
714 pPrint = PP.text . show
715 {-# INLINE pPrint #-}
716
717instance Pretty IP where
718 pPrint = PP.text . show
719 {-# INLINE pPrint #-}
720
721
722-- | When 'get'ing an IP it must be 'isolate'd to the appropriate
723-- number of bytes since we have no other way of telling which
724-- address type we are trying to parse
725instance Serialize IP where
726 put (IPv4 ip) = put ip
727 put (IPv6 ip) = put ip
728
729 get = do
730 n <- remaining
731 case n of
732 4 -> IPv4 <$> get
733 16 -> IPv6 <$> get
734 _ -> fail (show n ++ " is the wrong number of remaining bytes to parse IP")
735
736instance Serialize IPv4 where
737 put = putWord32host . toHostAddress
738 get = fromHostAddress <$> getWord32host
739
740instance Serialize IPv6 where
741 put ip = put $ toHostAddress6 ip
742 get = fromHostAddress6 <$> get
743
744
745instance Hashable IPv4 where
746 hashWithSalt = hashUsing toHostAddress
747 {-# INLINE hashWithSalt #-}
748
749instance Hashable IPv6 where
750 hashWithSalt s a = hashWithSalt s (toHostAddress6 a)
751
752instance Hashable IP where
753 hashWithSalt s (IPv4 h) = hashWithSalt s h
754 hashWithSalt s (IPv6 h) = hashWithSalt s h
755
756
757
758------------------------------------------------------------------------
759
760{-----------------------------------------------------------------------
761-- Fingerprint
762-----------------------------------------------------------------------}
763-- $fingerprint
764--
765-- 'Fingerprint' is used to identify the client implementation and
766-- version which also contained in 'Peer'. For exsample first 6
767-- bytes of peer id of this this library are @-HS0100-@ while for
768-- mainline we have @M4-3-6--@. We could extract this info and
769-- print in human-friendly form: this is useful for debugging and
770-- logging.
771--
772-- For more information see:
773-- <http://bittorrent.org/beps/bep_0020.html>
774--
775--
776-- NOTE: Do /not/ use this information to control client
777-- capabilities (such as supported enchancements), this should be
778-- done using 'Network.BitTorrent.Extension'!
779--
780
781-- TODO FIXME
782version :: Version
783version = Version [0, 0, 0, 3] []
784
785-- | List of registered client versions + 'IlibHSbittorrent' (this
786-- package) + 'IUnknown' (for not recognized software). All names are
787-- prefixed by \"I\" because some of them starts from lowercase letter
788-- but that is not a valid Haskell constructor name.
789--
790data Software =
791 IUnknown
792
793 | IMainline
794
795 | IABC
796 | IOspreyPermaseed
797 | IBTQueue
798 | ITribler
799 | IShadow
800 | IBitTornado
801
802-- UPnP(!) Bit Torrent !???
803-- 'U' - UPnP NAT Bit Torrent
804 | IBitLord
805 | IOpera
806 | IMLdonkey
807
808 | IAres
809 | IArctic
810 | IAvicora
811 | IBitPump
812 | IAzureus
813 | IBitBuddy
814 | IBitComet
815 | IBitflu
816 | IBTG
817 | IBitRocket
818 | IBTSlave
819 | IBittorrentX
820 | IEnhancedCTorrent
821 | ICTorrent
822 | IDelugeTorrent
823 | IPropagateDataClient
824 | IEBit
825 | IElectricSheep
826 | IFoxTorrent
827 | IGSTorrent
828 | IHalite
829 | IlibHSbittorrent
830 | IHydranode
831 | IKGet
832 | IKTorrent
833 | ILH_ABC
834 | ILphant
835 | ILibtorrent
836 | ILibTorrent
837 | ILimeWire
838 | IMonoTorrent
839 | IMooPolice
840 | IMiro
841 | IMoonlightTorrent
842 | INetTransport
843 | IPando
844 | IqBittorrent
845 | IQQDownload
846 | IQt4TorrentExample
847 | IRetriever
848 | IShareaza
849 | ISwiftbit
850 | ISwarmScope
851 | ISymTorrent
852 | Isharktorrent
853 | ITorrentDotNET
854 | ITransmission
855 | ITorrentstorm
856 | ITuoTu
857 | IuLeecher
858 | IuTorrent
859 | IVagaa
860 | IBitLet
861 | IFireTorrent
862 | IXunlei
863 | IXanTorrent
864 | IXtorrent
865 | IZipTorrent
866 deriving (Show, Eq, Ord, Enum, Bounded)
867
868parseSoftware :: ByteString -> Software
869parseSoftware = f . BC.unpack
870 where
871 f "AG" = IAres
872 f "A~" = IAres
873 f "AR" = IArctic
874 f "AV" = IAvicora
875 f "AX" = IBitPump
876 f "AZ" = IAzureus
877 f "BB" = IBitBuddy
878 f "BC" = IBitComet
879 f "BF" = IBitflu
880 f "BG" = IBTG
881 f "BR" = IBitRocket
882 f "BS" = IBTSlave
883 f "BX" = IBittorrentX
884 f "CD" = IEnhancedCTorrent
885 f "CT" = ICTorrent
886 f "DE" = IDelugeTorrent
887 f "DP" = IPropagateDataClient
888 f "EB" = IEBit
889 f "ES" = IElectricSheep
890 f "FT" = IFoxTorrent
891 f "GS" = IGSTorrent
892 f "HL" = IHalite
893 f "HS" = IlibHSbittorrent
894 f "HN" = IHydranode
895 f "KG" = IKGet
896 f "KT" = IKTorrent
897 f "LH" = ILH_ABC
898 f "LP" = ILphant
899 f "LT" = ILibtorrent
900 f "lt" = ILibTorrent
901 f "LW" = ILimeWire
902 f "MO" = IMonoTorrent
903 f "MP" = IMooPolice
904 f "MR" = IMiro
905 f "ML" = IMLdonkey
906 f "MT" = IMoonlightTorrent
907 f "NX" = INetTransport
908 f "PD" = IPando
909 f "qB" = IqBittorrent
910 f "QD" = IQQDownload
911 f "QT" = IQt4TorrentExample
912 f "RT" = IRetriever
913 f "S~" = IShareaza
914 f "SB" = ISwiftbit
915 f "SS" = ISwarmScope
916 f "ST" = ISymTorrent
917 f "st" = Isharktorrent
918 f "SZ" = IShareaza
919 f "TN" = ITorrentDotNET
920 f "TR" = ITransmission
921 f "TS" = ITorrentstorm
922 f "TT" = ITuoTu
923 f "UL" = IuLeecher
924 f "UT" = IuTorrent
925 f "VG" = IVagaa
926 f "WT" = IBitLet
927 f "WY" = IFireTorrent
928 f "XL" = IXunlei
929 f "XT" = IXanTorrent
930 f "XX" = IXtorrent
931 f "ZT" = IZipTorrent
932 f _ = IUnknown
933
934-- | Used to represent a not recognized implementation
935instance Default Software where
936 def = IUnknown
937 {-# INLINE def #-}
938
939-- | Example: @\"BitLet\" == 'IBitLet'@
940instance IsString Software where
941 fromString str
942 | Just impl <- L.lookup str alist = impl
943 | otherwise = error $ "fromString: not recognized " ++ str
944 where
945 alist = L.map mk [minBound..maxBound]
946 mk x = (L.tail $ show x, x)
947
948-- | Example: @pPrint 'IBitLet' == \"IBitLet\"@
949instance Pretty Software where
950 pPrint = text . L.tail . show
951
952-- | Just the '0' version.
953instance Default Version where
954 def = Version [0] []
955 {-# INLINE def #-}
956
957-- | For dot delimited version strings.
958-- Example: @fromString \"0.1.0.2\" == Version [0, 1, 0, 2]@
959--
960instance IsString Version where
961 fromString str
962 | Just nums <- chunkNums str = Version nums []
963 | otherwise = error $ "fromString: invalid version string " ++ str
964 where
965 chunkNums = sequence . L.map readMaybe . L.linesBy ('.' ==)
966
967instance Pretty Version where
968 pPrint = text . showVersion
969
970-- | The all sensible infomation that can be obtained from a peer
971-- identifier or torrent /createdBy/ field.
972data Fingerprint = Fingerprint Software Version
973 deriving (Show, Eq, Ord)
974
975-- | Unrecognized client implementation.
976instance Default Fingerprint where
977 def = Fingerprint def def
978 {-# INLINE def #-}
979
980-- | Example: @\"BitComet-1.2\" == ClientInfo IBitComet (Version [1, 2] [])@
981instance IsString Fingerprint where
982 fromString str
983 | _ : ver <- _ver = Fingerprint (fromString impl) (fromString ver)
984 | otherwise = error $ "fromString: invalid client info string" ++ str
985 where
986 (impl, _ver) = L.span ((/=) '-') str
987
988instance Pretty Fingerprint where
989 pPrint (Fingerprint s v) = pPrint s <+> "version" <+> pPrint v
990
991-- | Fingerprint of this (the bittorrent library) package. Normally,
992-- applications should introduce its own fingerprints, otherwise they
993-- can use 'libFingerprint' value.
994--
995libFingerprint :: Fingerprint
996libFingerprint = Fingerprint IlibHSbittorrent version
997
998-- | HTTP user agent of this (the bittorrent library) package. Can be
999-- used in HTTP tracker requests.
1000libUserAgent :: String
1001libUserAgent = render (pPrint IlibHSbittorrent <> "/" <> pPrint version)
1002
1003{-----------------------------------------------------------------------
1004-- For torrent file
1005-----------------------------------------------------------------------}
1006-- TODO collect information about createdBy torrent field
1007-- renderImpl :: ClientImpl -> Text
1008-- renderImpl = T.pack . L.tail . show
1009--
1010-- renderVersion :: Version -> Text
1011-- renderVersion = undefined
1012--
1013-- renderClientInfo :: ClientInfo -> Text
1014-- renderClientInfo ClientInfo {..} = renderImpl ciImpl <> "/" <> renderVersion ciVersion
1015--
1016-- parseClientInfo :: Text -> ClientImpl
1017-- parseClientInfo t = undefined
1018
1019
1020-- code used for generation; remove it later on
1021--
1022-- mkEnumTyDef :: NM -> String
1023-- mkEnumTyDef = unlines . map (" | I" ++) . nub . map snd
1024--
1025-- mkPars :: NM -> String
1026-- mkPars = unlines . map (\(code, impl) -> " f \"" ++ code ++ "\" = " ++ "I" ++ impl)
1027--
1028-- type NM = [(String, String)]
1029-- nameMap :: NM
1030-- nameMap =
1031-- [ ("AG", "Ares")
1032-- , ("A~", "Ares")
1033-- , ("AR", "Arctic")
1034-- , ("AV", "Avicora")
1035-- , ("AX", "BitPump")
1036-- , ("AZ", "Azureus")
1037-- , ("BB", "BitBuddy")
1038-- , ("BC", "BitComet")
1039-- , ("BF", "Bitflu")
1040-- , ("BG", "BTG")
1041-- , ("BR", "BitRocket")
1042-- , ("BS", "BTSlave")
1043-- , ("BX", "BittorrentX")
1044-- , ("CD", "EnhancedCTorrent")
1045-- , ("CT", "CTorrent")
1046-- , ("DE", "DelugeTorrent")
1047-- , ("DP", "PropagateDataClient")
1048-- , ("EB", "EBit")
1049-- , ("ES", "ElectricSheep")
1050-- , ("FT", "FoxTorrent")
1051-- , ("GS", "GSTorrent")
1052-- , ("HL", "Halite")
1053-- , ("HS", "libHSnetwork_bittorrent")
1054-- , ("HN", "Hydranode")
1055-- , ("KG", "KGet")
1056-- , ("KT", "KTorrent")
1057-- , ("LH", "LH_ABC")
1058-- , ("LP", "Lphant")
1059-- , ("LT", "Libtorrent")
1060-- , ("lt", "LibTorrent")
1061-- , ("LW", "LimeWire")
1062-- , ("MO", "MonoTorrent")
1063-- , ("MP", "MooPolice")
1064-- , ("MR", "Miro")
1065-- , ("MT", "MoonlightTorrent")
1066-- , ("NX", "NetTransport")
1067-- , ("PD", "Pando")
1068-- , ("qB", "qBittorrent")
1069-- , ("QD", "QQDownload")
1070-- , ("QT", "Qt4TorrentExample")
1071-- , ("RT", "Retriever")
1072-- , ("S~", "Shareaza")
1073-- , ("SB", "Swiftbit")
1074-- , ("SS", "SwarmScope")
1075-- , ("ST", "SymTorrent")
1076-- , ("st", "sharktorrent")
1077-- , ("SZ", "Shareaza")
1078-- , ("TN", "TorrentDotNET")
1079-- , ("TR", "Transmission")
1080-- , ("TS", "Torrentstorm")
1081-- , ("TT", "TuoTu")
1082-- , ("UL", "uLeecher")
1083-- , ("UT", "uTorrent")
1084-- , ("VG", "Vagaa")
1085-- , ("WT", "BitLet")
1086-- , ("WY", "FireTorrent")
1087-- , ("XL", "Xunlei")
1088-- , ("XT", "XanTorrent")
1089-- , ("XX", "Xtorrent")
1090-- , ("ZT", "ZipTorrent")
1091-- ]
1092
1093-- TODO use regexps
1094
1095-- | Tries to extract meaningful information from peer ID bytes. If
1096-- peer id uses unknown coding style then client info returned is
1097-- 'def'.
1098--
1099fingerprint :: PeerId -> Fingerprint
1100fingerprint pid = either (const def) id $ runGet getCI (getPeerId pid)
1101 where
1102 getCI = do
1103 leading <- BS.w2c <$> getWord8
1104 case leading of
1105 '-' -> Fingerprint <$> getAzureusImpl <*> getAzureusVersion
1106 'M' -> Fingerprint <$> pure IMainline <*> getMainlineVersion
1107 'e' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
1108 'F' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
1109 c -> do
1110 c1 <- BS.w2c <$> S.lookAhead getWord8
1111 if c1 == 'P'
1112 then do
1113 _ <- getWord8
1114 Fingerprint <$> pure IOpera <*> getOperaVersion
1115 else Fingerprint <$> pure (getShadowImpl c) <*> getShadowVersion
1116
1117 getMainlineVersion = do
1118 str <- BC.unpack <$> getByteString 7
1119 let mnums = L.filter (not . L.null) $ L.linesBy ('-' ==) str
1120 return $ Version (fromMaybe [] $ sequence $ L.map readMaybe mnums) []
1121
1122 getAzureusImpl = parseSoftware <$> getByteString 2
1123 getAzureusVersion = mkVer <$> getByteString 4
1124 where
1125 mkVer bs = Version [fromMaybe 0 $ readMaybe $ BC.unpack bs] []
1126
1127 getBitCometImpl = do
1128 bs <- getByteString 3
1129 S.lookAhead $ do
1130 _ <- getByteString 2
1131 lr <- getByteString 4
1132 return $
1133 if lr == "LORD" then IBitLord else
1134 if bs == "UTB" then IBitComet else
1135 if bs == "xbc" then IBitComet else def
1136
1137 getBitCometVersion = do
1138 x <- getWord8
1139 y <- getWord8
1140 return $ Version [fromIntegral x, fromIntegral y] []
1141
1142 getOperaVersion = do
1143 str <- BC.unpack <$> getByteString 4
1144 return $ Version [fromMaybe 0 $ readMaybe str] []
1145
1146 getShadowImpl 'A' = IABC
1147 getShadowImpl 'O' = IOspreyPermaseed
1148 getShadowImpl 'Q' = IBTQueue
1149 getShadowImpl 'R' = ITribler
1150 getShadowImpl 'S' = IShadow
1151 getShadowImpl 'T' = IBitTornado
1152 getShadowImpl _ = IUnknown
1153
1154 decodeShadowVerNr :: Char -> Maybe Int
1155 decodeShadowVerNr c
1156 | '0' < c && c <= '9' = Just (fromEnum c - fromEnum '0')
1157 | 'A' < c && c <= 'Z' = Just ((fromEnum c - fromEnum 'A') + 10)
1158 | 'a' < c && c <= 'z' = Just ((fromEnum c - fromEnum 'a') + 36)
1159 | otherwise = Nothing
1160
1161 getShadowVersion = do
1162 str <- BC.unpack <$> getByteString 5
1163 return $ Version (catMaybes $ L.map decodeShadowVerNr str) []
1164
1165
1166
1167-- | Given a string specifying a port (numeric or service name)
1168-- and a flag indicating whether you want to support IPv6, this
1169-- function will return a SockAddr to bind to. If the input
1170-- is not understood as a port number, zero will be set in order
1171-- to ask the system for an unused port.
1172getBindAddress :: String -> Bool -> IO SockAddr
1173getBindAddress bindspec enabled6 = do
1174 let (host,listenPortString) = case L.break (==':') (L.reverse bindspec) of
1175 (rport,':':rhost) -> (Just $ L.reverse rhost, L.reverse rport)
1176 _ -> (Nothing, bindspec)
1177 -- Bind addresses for localhost
1178 xs <- getAddrInfo (Just (defaultHints {addrFlags = [AI_PASSIVE] }))
1179 host
1180 (Just listenPortString)
1181 `onException` return []
1182 -- We prefer IPv6 because that can also handle connections from IPv4
1183 -- clients...
1184 let (x6s,x4s) = partition (\s -> addrFamily s == AF_INET6) xs
1185 listenAddr =
1186 case if enabled6 then x6s++x4s else x4s of
1187 AddrInfo { addrAddress = addr } : _ -> addr
1188 _ -> if enabled6
1189 then SockAddrInet6 (parsePort listenPortString) 0 iN6ADDR_ANY 0
1190 else SockAddrInet (parsePort listenPortString) iNADDR_ANY
1191 where parsePort s = fromMaybe 0 $ readMaybe s
1192 hPutStrLn stderr $ BS8.pack $ "Listening on " ++ show listenAddr
1193 return listenAddr
1194
1195-- | True if the argument is an IPv4-mapped address with prefix ::FFFF:0:0/96
1196-- as defined in RFC 4291.
1197is4mapped :: IPv6 -> Bool
1198is4mapped ip
1199 | [0,0,0,0,0,0xffff,_,_] <- fromIPv6 ip
1200 = True
1201 | otherwise = False
1202
1203un4map :: IPv6 -> Maybe IPv4
1204un4map ip
1205 | [0,0,0,0,0,0xffff,x,y] <- fromIPv6 ip
1206 = Just $ toIPv4
1207 $ L.map (.&. 0xFF)
1208 [x `shiftR` 8, x, y `shiftR` 8, y ]
1209 | otherwise = Nothing
1210
1211ipFamily :: IP -> WantIP
1212ipFamily ip = case ip of
1213 IPv4 _ -> Want_IP4
1214 IPv6 a | is4mapped a -> Want_IP4
1215 | otherwise -> Want_IP6
1216
1217either4or6 :: SockAddr -> Either SockAddr SockAddr
1218either4or6 a4@(SockAddrInet port addr) = Left a4
1219either4or6 a6@(SockAddrInet6 port _ addr _)
1220 | Just ip4 <- (fromSockAddr a6 >>= un4map) = Left (setPort port $ toSockAddr ip4)
1221 | otherwise = Right a6
1222
1223data WantIP = Want_IP4 | Want_IP6 | Want_Both
1224 deriving (Eq, Enum, Ord, Show)
1225
diff --git a/src/Network/BitTorrent/DHT/ContactInfo.hs b/src/Network/BitTorrent/DHT/ContactInfo.hs
new file mode 100644
index 00000000..c8187772
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/ContactInfo.hs
@@ -0,0 +1,236 @@
1{-# LANGUAGE BangPatterns #-}
2module Network.BitTorrent.DHT.ContactInfo
3 ( PeerStore
4 , PeerAddr(..)
5 , Network.BitTorrent.DHT.ContactInfo.lookup
6 , Network.BitTorrent.DHT.ContactInfo.freshPeers
7 , Network.BitTorrent.DHT.ContactInfo.insertPeer
8 , knownSwarms
9 ) where
10
11import Control.Applicative
12import Data.Default
13import Data.List as L
14import Data.Maybe
15import Data.HashMap.Strict as HM
16import Data.Serialize
17import Data.Wrapper.PSQ as PSQ
18import Data.Time.Clock.POSIX
19import Data.ByteString (ByteString)
20import Data.Word
21import Network.Socket (SockAddr(..))
22
23import Data.Torrent
24import Network.Address
25
26-- {-
27-- import Data.HashMap.Strict as HM
28--
29-- import Data.Torrent.InfoHash
30-- import Network.Address
31--
32-- -- increase prefix when table is too large
33-- -- decrease prefix when table is too small
34-- -- filter outdated peers
35--
36-- {-----------------------------------------------------------------------
37-- -- PeerSet
38-- -----------------------------------------------------------------------}
39--
40-- type PeerSet a = [(PeerAddr, NodeInfo a, Timestamp)]
41--
42-- -- compare PSQueue vs Ordered list
43--
44-- takeNewest :: PeerSet a -> [PeerAddr]
45-- takeNewest = undefined
46--
47-- dropOld :: Timestamp -> PeerSet a -> PeerSet a
48-- dropOld = undefined
49--
50-- insert :: PeerAddr -> Timestamp -> PeerSet a -> PeerSet a
51-- insert = undefined
52--
53-- type Mask = Int
54-- type Size = Int
55-- type Timestamp = Int
56--
57-- {-----------------------------------------------------------------------
58-- -- InfoHashMap
59-- -----------------------------------------------------------------------}
60--
61-- -- compare handwritten prefix tree versus IntMap
62--
63-- data Tree a
64-- = Nil
65-- | Tip !InfoHash !(PeerSet a)
66-- | Bin !InfoHash !Mask !Size !Timestamp (Tree a) (Tree a)
67--
68-- insertTree :: InfoHash -> a -> Tree a -> Tree a
69-- insertTree = undefined
70--
71-- type Prio = Int
72--
73-- --shrink :: ContactInfo ip -> Int
74-- shrink Nil = Nil
75-- shrink (Tip _ _) = undefined
76-- shrink (Bin _ _) = undefined
77--
78-- {-----------------------------------------------------------------------
79-- -- InfoHashMap
80-- -----------------------------------------------------------------------}
81--
82-- -- compare new design versus HashMap
83--
84-- data IntMap k p a
85-- type ContactInfo = Map InfoHash Timestamp (Set (PeerAddr IP) Timestamp)
86--
87-- data ContactInfo ip = PeerStore
88-- { maxSize :: Int
89-- , prefixSize :: Int
90-- , thisNodeId :: NodeId
91--
92-- , count :: Int -- ^ Cached size of the 'peerSet'
93-- , peerSet :: HashMap InfoHash [PeerAddr ip]
94-- }
95--
96-- size :: ContactInfo ip -> Int
97-- size = undefined
98--
99-- prefixSize :: ContactInfo ip -> Int
100-- prefixSize = undefined
101--
102-- lookup :: InfoHash -> ContactInfo ip -> [PeerAddr ip]
103-- lookup = undefined
104--
105-- insert :: InfoHash -> PeerAddr ip -> ContactInfo ip -> ContactInfo ip
106-- insert = undefined
107--
108-- -- | Limit in size.
109-- prune :: NodeId -> Int -> ContactInfo ip -> ContactInfo ip
110-- prune pref targetSize Nil = Nil
111-- prune pref targetSize (Tip _ _) = undefined
112--
113-- -- | Remove expired entries.
114-- splitGT :: Timestamp -> ContactInfo ip -> ContactInfo ip
115-- splitGT = undefined
116-- -}
117
118-- | Storage used to keep track a set of known peers in client,
119-- tracker or DHT sessions.
120newtype PeerStore = PeerStore (HashMap InfoHash SwarmData)
121
122type Timestamp = POSIXTime
123
124data SwarmData = SwarmData
125 { peers :: !(PSQ PeerAddr Timestamp)
126 , name :: !(Maybe ByteString)
127 }
128
129-- | This wrapper will serialize an ip address with a '4' or '6' prefix byte
130-- to indicate whether it is IPv4 or IPv6.
131--
132-- Note: it does not serialize port numbers.
133newtype SerializeAddress a = SerializeAddress { unserializeAddress :: a }
134
135instance Address a => Serialize (SerializeAddress a) where
136 get = SerializeAddress <$> do
137 c <- get
138 case (c::Word8) of
139 0x34 -> do ip4 <- get
140 return $ fromJust $ fromAddr (ip4::IPv4)
141 0x36 -> do ip6 <- get
142 return $ fromJust $ fromAddr (ip6::IPv6)
143 _ -> return $ error "cannot deserialize non-IP SerializeAddress"
144 put (SerializeAddress a)
145 | Just ip4 <- fromAddr a
146 = put (0x34::Word8) >> put (ip4::IPv4)
147 | Just ip6 <- fromAddr a
148 = put (0x36::Word8) >> put (ip6::IPv6)
149 | otherwise = return $ error "cannot serialize non-IP SerializeAddress"
150
151
152instance Serialize SwarmData where
153 get = flip SwarmData <$> get
154 <*> ( PSQ.fromList . L.map parseAddr <$> get )
155 where
156 parseAddr (pid,addr,port) = PeerAddr { peerId = pid
157 , peerHost = unserializeAddress addr
158 , peerPort = port
159 }
160 :-> 0
161
162 put SwarmData{..} = do
163 put name
164 put $ L.map (\(addr :-> _) -> (peerId addr, SerializeAddress addr, peerPort addr))
165 -- XXX: should we serialize the timestamp?
166 $ PSQ.toList peers
167
168knownSwarms :: PeerStore -> [ (InfoHash, Int, Maybe ByteString) ]
169knownSwarms (PeerStore m) = L.map (\(ih,SwarmData q n) -> (ih,PSQ.size q,n)) $ HM.toList m
170
171swarmSingleton :: PeerAddr -> SwarmData
172swarmSingleton a = SwarmData
173 { peers = PSQ.singleton a 0
174 , name = Nothing }
175
176swarmInsert :: SwarmData -> SwarmData -> SwarmData
177swarmInsert new old = SwarmData
178 { peers = L.foldl' (\q (a :-> t) -> PSQ.insertWith newerTimeStamp a t q) (peers old) (PSQ.toList $ peers new)
179 , name = name new <|> name old -- TODO: decodeUtf8' check
180 }
181 where
182 newerTimeStamp newtime oldtime = if newtime > oldtime then newtime else oldtime
183
184isSwarmOccupied :: SwarmData -> Bool
185isSwarmOccupied SwarmData{..} = not $ PSQ.null peers
186
187-- | Empty store.
188instance Default (PeerStore) where
189 def = PeerStore HM.empty
190 {-# INLINE def #-}
191
192-- | Monoid under union operation.
193instance Monoid PeerStore where
194 mempty = def
195 {-# INLINE mempty #-}
196
197 mappend (PeerStore a) (PeerStore b) =
198 PeerStore (HM.unionWith swarmInsert a b)
199 {-# INLINE mappend #-}
200
201-- | Can be used to store peers between invocations of the client
202-- software.
203instance Serialize PeerStore where
204 get = PeerStore . HM.fromList <$> get
205 put (PeerStore m) = put (L.filter (isSwarmOccupied . snd) $ HM.toList m)
206
207-- | Returns all peers associated with a given info hash.
208lookup :: InfoHash -> PeerStore -> [PeerAddr]
209lookup ih (PeerStore m) = maybe [] (PSQ.keys . peers) $ HM.lookup ih m
210
211batchSize :: Int
212batchSize = 64
213
214-- | Used in 'get_peers' DHT queries.
215freshPeers :: InfoHash -> Timestamp -> PeerStore -> ([PeerAddr], PeerStore)
216freshPeers ih tm (PeerStore m) = fromMaybe ([],PeerStore m) $ do
217 swarm <- HM.lookup ih m
218 let ps0 = take batchSize $ unfoldr (incomp minView) (peers swarm)
219 peers' = case reverse ps0 of
220 (_,psq):_ -> psq
221 _ -> peers swarm
222 ps = L.map (key . fst) ps0
223 m' = HM.insert ih swarm { peers = L.foldl' (\q p -> PSQ.insert p tm q) peers' ps } m
224 return $! m' `seq` (ps,PeerStore m')
225
226incomp :: (x -> Maybe (r,x)) -> x -> Maybe ((r,x),x)
227incomp !f !x = do
228 (result,x') <- f x
229 pure $! ( (result,x'), x' )
230
231-- | Used in 'announce_peer' DHT queries.
232insertPeer :: InfoHash -> Maybe ByteString -> PeerAddr -> PeerStore -> PeerStore
233insertPeer !ih !name !a !(PeerStore m) = seq a' $ PeerStore (HM.insertWith swarmInsert ih a' m)
234 where
235 a' = SwarmData { peers = PSQ.singleton a 0
236 , name = name }
diff --git a/src/Network/BitTorrent/DHT/Readme.md b/src/Network/BitTorrent/DHT/Readme.md
new file mode 100644
index 00000000..e2352f10
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Readme.md
@@ -0,0 +1,13 @@
1References
2==========
3
4Some good references excluding BEPs:
5
6* [Kademlia wiki page][kademlia-wiki]
7* [Kademlia: A Peer-to-peer Information System Based on the XOR Metric][kademlia-paper]
8* [BitTorrent Mainline DHT Measurement][mldht]
9* Profiling a Million User DHT. (paper)
10
11[kademlia-wiki]: http://en.wikipedia.org/wiki/Kademlia
12[kademlia-paper]: http://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
13[mldht]: http://www.cs.helsinki.fi/u/jakangas/MLDHT/
diff --git a/src/Network/BitTorrent/DHT/Token.hs b/src/Network/BitTorrent/DHT/Token.hs
new file mode 100644
index 00000000..3da59c53
--- /dev/null
+++ b/src/Network/BitTorrent/DHT/Token.hs
@@ -0,0 +1,202 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- The return value for a query for peers includes an opaque value
9-- known as the 'Token'. For a node to announce that its controlling
10-- peer is downloading a torrent, it must present the token received
11-- from the same queried node in a recent query for peers. When a node
12-- attempts to \"announce\" a torrent, the queried node checks the
13-- token against the querying node's 'IP' address. This is to prevent
14-- malicious hosts from signing up other hosts for torrents. Since the
15-- token is merely returned by the querying node to the same node it
16-- received the token from, the implementation is not defined. Tokens
17-- must be accepted for a reasonable amount of time after they have
18-- been distributed.
19--
20{-# LANGUAGE GeneralizedNewtypeDeriving, CPP #-}
21module Network.BitTorrent.DHT.Token
22 ( -- * Token
23 Token
24 , maxInterval
25 , toPaddedByteString
26 , fromPaddedByteString
27
28 -- * Session tokens
29 , TokenMap
30 , SessionTokens
31 , nullSessionTokens
32 , checkToken
33 , grantToken
34
35 -- ** Construction
36 , Network.BitTorrent.DHT.Token.tokens
37
38 -- ** Query
39 , Network.BitTorrent.DHT.Token.lookup
40 , Network.BitTorrent.DHT.Token.member
41
42 -- ** Modification
43 , Network.BitTorrent.DHT.Token.defaultUpdateInterval
44 , Network.BitTorrent.DHT.Token.update
45 ) where
46
47import Control.Arrow
48import Control.Monad.State
49#ifdef VERSION_bencoding
50import Data.BEncode (BEncode)
51#endif
52import Data.ByteString as BS
53import Data.ByteString.Char8 as B8
54import Data.ByteString.Lazy as BL
55import Data.ByteString.Lazy.Builder as BS
56import qualified Data.ByteString.Base16 as Base16
57import Data.Default
58import Data.List as L
59import Data.Hashable
60import Data.String
61import Data.Time
62import System.Random
63import Control.Concurrent.STM
64import Network.Address
65
66-- TODO use ShortByteString
67
68-- | An opaque value.
69newtype Token = Token BS.ByteString
70 deriving ( Eq, IsString
71#ifdef VERSION_bencoding
72 , BEncode
73#endif
74 )
75
76instance Show Token where
77 show (Token bs) = B8.unpack $ Base16.encode bs
78
79instance Read Token where
80 readsPrec i s = pure $ (Token *** B8.unpack) $ Base16.decode (B8.pack s)
81
82-- | Meaningless token, for testing purposes only.
83instance Default Token where
84 def = makeToken (0::Int) 0
85
86-- | Prepend token with 0x20 bytes to fill the available width.
87--
88-- If n > 8, then this will also guarantee a nonzero token, which is useful for
89-- Tox ping-id values for announce responses.
90toPaddedByteString :: Int -> Token -> BS.ByteString
91toPaddedByteString n (Token bs) = BS.append (BS.replicate (n - BS.length bs) 0x20) bs
92
93fromPaddedByteString :: Int -> BS.ByteString -> Token
94fromPaddedByteString n bs = Token $ BS.drop (n - len) bs
95 where
96 len = BS.length tok where Token tok = def
97
98-- | The secret value used as salt.
99type Secret = Int
100
101-- The BitTorrent implementation uses the SHA1 hash of the IP address
102-- concatenated onto a secret, we use hashable instead.
103makeToken :: Hashable a => a -> Secret -> Token
104makeToken n s = Token $ toBS $ hashWithSalt s n
105 where
106 toBS = toStrict . toLazyByteString . int64BE . fromIntegral
107{-# INLINE makeToken #-}
108
109-- | Constant space 'Node' to 'Token' map based on the secret value.
110data TokenMap = TokenMap
111 { prevSecret :: {-# UNPACK #-} !Secret
112 , curSecret :: {-# UNPACK #-} !Secret
113 , generator :: {-# UNPACK #-} !StdGen
114 } deriving Show
115
116-- | A new token map based on the specified seed value. Returned token
117-- map should be periodicatically 'update'd.
118--
119-- Normally, the seed value should vary between invocations of the
120-- client software.
121tokens :: Int -> TokenMap
122tokens seed = (`evalState` mkStdGen seed) $
123 TokenMap <$> state next
124 <*> state next
125 <*> get
126
127-- | Get token for the given node. A token becomes invalid after 2
128-- 'update's.
129--
130-- Typically used to handle find_peers query.
131lookup :: Hashable a => a -> TokenMap -> Token
132lookup addr TokenMap {..} = makeToken addr curSecret
133
134-- | Check if token is valid.
135--
136-- Typically used to handle 'Network.DHT.Mainline.Announce'
137-- query. If token is invalid the 'Network.KRPC.ProtocolError' should
138-- be sent back to the malicious node.
139member :: Hashable a => a -> Token -> TokenMap -> Bool
140member addr token TokenMap {..} = token `L.elem` valid
141 where valid = makeToken addr <$> [curSecret, prevSecret]
142
143-- | Secret changes every five minutes and tokens up to ten minutes old
144-- are accepted.
145defaultUpdateInterval :: NominalDiffTime
146defaultUpdateInterval = 5 * 60
147
148-- | Update current tokens.
149update :: TokenMap -> TokenMap
150update TokenMap {..} = TokenMap
151 { prevSecret = curSecret
152 , curSecret = newSecret
153 , generator = newGen
154 }
155 where
156 (newSecret, newGen) = next generator
157
158data SessionTokens = SessionTokens
159 { tokenMap :: !TokenMap
160 , lastUpdate :: !UTCTime
161 , maxInterval :: !NominalDiffTime
162 }
163
164nullSessionTokens :: IO SessionTokens
165nullSessionTokens = SessionTokens
166 <$> (tokens <$> randomIO)
167 <*> getCurrentTime
168 <*> pure defaultUpdateInterval
169
170-- TODO invalidate *twice* if needed
171invalidateTokens :: UTCTime -> SessionTokens -> SessionTokens
172invalidateTokens curTime ts @ SessionTokens {..}
173 | curTime `diffUTCTime` lastUpdate > maxInterval = SessionTokens
174 { tokenMap = update tokenMap
175 , lastUpdate = curTime
176 , maxInterval = maxInterval
177 }
178 | otherwise = ts
179
180{-----------------------------------------------------------------------
181-- Tokens
182-----------------------------------------------------------------------}
183
184tryUpdateSecret :: TVar SessionTokens -> IO ()
185tryUpdateSecret toks = do
186 curTime <- getCurrentTime
187 atomically $ modifyTVar' toks (invalidateTokens curTime)
188
189grantToken :: Hashable addr => TVar SessionTokens -> addr -> IO Token
190grantToken sessionTokens addr = do
191 tryUpdateSecret sessionTokens
192 toks <- readTVarIO sessionTokens
193 return $ Network.BitTorrent.DHT.Token.lookup addr $ tokenMap toks
194
195-- | Throws 'HandlerError' if the token is invalid or already
196-- expired. See 'TokenMap' for details.
197checkToken :: Hashable addr => TVar SessionTokens -> addr -> Token -> IO Bool
198checkToken sessionTokens addr questionableToken = do
199 tryUpdateSecret sessionTokens
200 toks <- readTVarIO sessionTokens
201 return $ member addr questionableToken (tokenMap toks)
202
diff --git a/src/Network/BitTorrent/MainlineDHT.hs b/src/Network/BitTorrent/MainlineDHT.hs
new file mode 100644
index 00000000..f43b070c
--- /dev/null
+++ b/src/Network/BitTorrent/MainlineDHT.hs
@@ -0,0 +1,1111 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3{-# LANGUAGE DeriveFoldable #-}
4{-# LANGUAGE DeriveFunctor #-}
5{-# LANGUAGE DeriveTraversable #-}
6{-# LANGUAGE FlexibleInstances #-}
7{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE NamedFieldPuns #-}
10{-# LANGUAGE PatternSynonyms #-}
11{-# LANGUAGE StandaloneDeriving #-}
12{-# LANGUAGE TupleSections #-}
13module Network.BitTorrent.MainlineDHT where
14
15import Control.Applicative
16import Control.Arrow
17import Control.Concurrent.STM
18import Control.Monad
19import Crypto.Random
20import Data.BEncode as BE
21import qualified Data.BEncode.BDict as BE
22 ;import Data.BEncode.BDict (BKey)
23import Data.BEncode.Pretty
24import Data.BEncode.Types (BDict)
25import Data.Bits
26import Data.Bits.ByteString
27import Data.Bool
28import qualified Data.ByteArray as BA
29 ;import Data.ByteArray (ByteArrayAccess)
30import qualified Data.ByteString as B
31 ;import Data.ByteString (ByteString)
32import qualified Data.ByteString.Base16 as Base16
33import qualified Data.ByteString.Char8 as C8
34import Data.ByteString.Lazy (toStrict)
35import qualified Data.ByteString.Lazy.Char8 as L8
36import Data.Char
37import Data.Coerce
38import Data.Data
39import Data.Default
40import Data.Digest.CRC32C
41import Data.Function (fix)
42import Data.Hashable
43import Data.IP
44import Data.List
45import Data.Maybe
46import Data.Monoid
47import Data.Ord
48import qualified Data.Serialize as S
49import Data.Set (Set)
50import Data.Time.Clock.POSIX (POSIXTime)
51import Data.Torrent
52import Data.Typeable
53import Data.Word
54import qualified Data.Wrapper.PSQInt as Int
55import Debug.Trace
56import Network.BitTorrent.MainlineDHT.Symbols
57import Network.Kademlia
58import Network.Kademlia.Bootstrap
59import Network.Address (Address, fromAddr, fromSockAddr,
60 setPort, sockAddrPort, testIdBit,
61 toSockAddr, genBucketSample', WantIP(..),
62 un4map,either4or6,ipFamily)
63import Network.BitTorrent.DHT.ContactInfo as Peers
64import Network.Kademlia.Search (Search (..))
65import Network.BitTorrent.DHT.Token as Token
66import qualified Network.Kademlia.Routing as R
67 ;import Network.Kademlia.Routing (Timestamp, getTimestamp)
68import Network.QueryResponse
69import Network.Socket
70import System.IO
71import System.IO.Error
72import System.IO.Unsafe (unsafeInterleaveIO)
73import qualified Text.ParserCombinators.ReadP as RP
74#ifdef THREAD_DEBUG
75import Control.Concurrent.Lifted.Instrument
76#else
77import Control.Concurrent.Lifted
78import GHC.Conc (labelThread)
79#endif
80import Control.Exception (SomeException (..), handle)
81import qualified Data.Aeson as JSON
82 ;import Data.Aeson (FromJSON, ToJSON, (.=))
83import Text.Read
84import System.Global6
85import Control.TriadCommittee
86
87newtype NodeId = NodeId ByteString
88 deriving (Eq,Ord,ByteArrayAccess, Bits, Hashable)
89
90instance BEncode NodeId where
91 fromBEncode bval = do
92 bs <- fromBEncode bval
93 if B.length bs /= 20
94 then Left "Invalid length node id."
95 else Right $ NodeId bs
96
97 toBEncode (NodeId bs) = toBEncode bs
98
99instance Show NodeId where
100 show (NodeId bs) = C8.unpack $ Base16.encode bs
101
102instance S.Serialize NodeId where
103 get = NodeId <$> S.getBytes 20
104 put (NodeId bs) = S.putByteString bs
105
106instance FiniteBits NodeId where
107 finiteBitSize _ = 160
108
109instance Read NodeId where
110 readsPrec _ str
111 | (bs, xs) <- Base16.decode $ C8.pack str
112 , B.length bs == 20
113 = [ (NodeId bs, drop 40 str) ]
114 | otherwise = []
115
116zeroID :: NodeId
117zeroID = NodeId $ B.replicate 20 0
118
119data NodeInfo = NodeInfo
120 { nodeId :: NodeId
121 , nodeIP :: IP
122 , nodePort :: PortNumber
123 }
124 deriving (Eq,Ord)
125
126instance ToJSON NodeInfo where
127 toJSON (NodeInfo nid (IPv4 ip) port)
128 = JSON.object [ "node-id" .= show nid
129 , "ipv4" .= show ip
130 , "port" .= (fromIntegral port :: Int)
131 ]
132 toJSON (NodeInfo nid (IPv6 ip6) port)
133 | Just ip <- un4map ip6
134 = JSON.object [ "node-id" .= show nid
135 , "ipv4" .= show ip
136 , "port" .= (fromIntegral port :: Int)
137 ]
138 | otherwise
139 = JSON.object [ "node-id" .= show nid
140 , "ipv6" .= show ip6
141 , "port" .= (fromIntegral port :: Int)
142 ]
143instance FromJSON NodeInfo where
144 parseJSON (JSON.Object v) = do
145 nidstr <- v JSON..: "node-id"
146 ip6str <- v JSON..:? "ipv6"
147 ip4str <- v JSON..:? "ipv4"
148 portnum <- v JSON..: "port"
149 ip <- maybe empty (return . IPv6) (ip6str >>= readMaybe)
150 <|> maybe empty (return . IPv4) (ip4str >>= readMaybe)
151 let (bs,_) = Base16.decode (C8.pack nidstr)
152 guard (B.length bs == 20)
153 return $ NodeInfo (NodeId bs) ip (fromIntegral (portnum :: Word16))
154
155hexdigit :: Char -> Bool
156hexdigit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'f') || ( 'A' <= c && c <= 'F')
157
158instance Read NodeInfo where
159 readsPrec i = RP.readP_to_S $ do
160 RP.skipSpaces
161 let n = 40 -- characters in node id.
162 parseAddr = RP.between (RP.char '(') (RP.char ')') (RP.munch (/=')'))
163 RP.+++ RP.munch (not . isSpace)
164 nodeidAt = do hexhash <- sequence $ replicate n (RP.satisfy hexdigit)
165 RP.char '@' RP.+++ RP.satisfy isSpace
166 addrstr <- parseAddr
167 nid <- case Base16.decode $ C8.pack hexhash of
168 (bs,_) | B.length bs==20 -> return (NodeId bs)
169 _ -> fail "Bad node id."
170 return (nid,addrstr)
171 (nid,addrstr) <- ( nodeidAt RP.+++ ( (zeroID,) <$> parseAddr) )
172 let raddr = do
173 ip <- RP.between (RP.char '[') (RP.char ']')
174 (IPv6 <$> RP.readS_to_P (readsPrec i))
175 RP.+++ (IPv4 <$> RP.readS_to_P (readsPrec i))
176 _ <- RP.char ':'
177 port <- toEnum <$> RP.readS_to_P (readsPrec i)
178 return (ip, port)
179
180 (ip,port) <- case RP.readP_to_S raddr addrstr of
181 [] -> fail "Bad address."
182 ((ip,port),_):_ -> return (ip,port)
183 return $ NodeInfo nid ip port
184
185
186
187-- The Hashable instance depends only on the IP address and port number. It is
188-- used to compute the announce token.
189instance Hashable NodeInfo where
190 hashWithSalt s ni = hashWithSalt s (nodeIP ni , nodePort ni)
191 {-# INLINE hashWithSalt #-}
192
193
194instance Show NodeInfo where
195 showsPrec _ (NodeInfo nid ip port) =
196 shows nid . ('@' :) . showsip . (':' :) . shows port
197 where
198 showsip
199 | IPv4 ip4 <- ip = shows ip4
200 | IPv6 ip6 <- ip , Just ip4 <- un4map ip6 = shows ip4
201 | otherwise = ('[' :) . shows ip . (']' :)
202
203{-
204
205-- | KRPC 'compact list' compatible encoding: contact information for
206-- nodes is encoded as a 26-byte string. Also known as "Compact node
207-- info" the 20-byte Node ID in network byte order has the compact
208-- IP-address/port info concatenated to the end.
209 get = NodeInfo <$> (NodeId <$> S.getBytes 20 ) <*> S.get <*> S.get
210-}
211
212getNodeInfo4 :: S.Get NodeInfo
213getNodeInfo4 = NodeInfo <$> (NodeId <$> S.getBytes 20)
214 <*> (IPv4 <$> S.get)
215 <*> S.get
216
217putNodeInfo4 :: NodeInfo -> S.Put
218putNodeInfo4 (NodeInfo (NodeId nid) ip port)
219 | IPv4 ip4 <- ip = put4 ip4
220 | IPv6 ip6 <- ip , Just ip4 <- un4map ip6 = put4 ip4
221 | otherwise = return ()
222 where
223 put4 ip4 = S.putByteString nid >> S.put ip4 >> S.put port
224
225getNodeInfo6 :: S.Get NodeInfo
226getNodeInfo6 = NodeInfo <$> (NodeId <$> S.getBytes 20)
227 <*> (IPv6 <$> S.get)
228 <*> S.get
229
230putNodeInfo6 :: NodeInfo -> S.Put
231putNodeInfo6 (NodeInfo (NodeId nid) (IPv6 ip) port)
232 = S.putByteString nid >> S.put ip >> S.put port
233putNodeInfo6 _ = return ()
234
235
236-- | TODO: This should depend on the bind address to support IPv4-only. For
237-- now, in order to support dual-stack listen, we're going to assume IPv6 is
238-- wanted and map IPv4 addresses accordingly.
239nodeAddr :: NodeInfo -> SockAddr
240nodeAddr (NodeInfo _ ip port) =
241 case ip of
242 IPv4 ip4 -> setPort port $ toSockAddr (ipv4ToIPv6 ip4)
243 IPv6 ip6 -> setPort port $ toSockAddr ip6
244
245nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo
246nodeInfo nid saddr
247 | Just ip <- fromSockAddr saddr
248 , Just port <- sockAddrPort saddr = Right $ NodeInfo nid ip port
249 | otherwise = Left "Address family not supported."
250
251-- | Types of RPC errors.
252data ErrorCode
253 -- | Some error doesn't fit in any other category.
254 = GenericError
255
256 -- | Occurs when server fail to process procedure call.
257 | ServerError
258
259 -- | Malformed packet, invalid arguments or bad token.
260 | ProtocolError
261
262 -- | Occurs when client trying to call method server don't know.
263 | MethodUnknown
264 deriving (Show, Read, Eq, Ord, Bounded, Typeable, Data)
265
266-- | According to the table:
267-- <http://bittorrent.org/beps/bep_0005.html#errors>
268instance Enum ErrorCode where
269 fromEnum GenericError = 201
270 fromEnum ServerError = 202
271 fromEnum ProtocolError = 203
272 fromEnum MethodUnknown = 204
273 {-# INLINE fromEnum #-}
274 toEnum 201 = GenericError
275 toEnum 202 = ServerError
276 toEnum 203 = ProtocolError
277 toEnum 204 = MethodUnknown
278 toEnum _ = GenericError
279 {-# INLINE toEnum #-}
280
281instance BEncode ErrorCode where
282 toBEncode = toBEncode . fromEnum
283 {-# INLINE toBEncode #-}
284 fromBEncode b = toEnum <$> fromBEncode b
285 {-# INLINE fromBEncode #-}
286
287data Error = Error
288 { errorCode :: !ErrorCode -- ^ The type of error.
289 , errorMessage :: !ByteString -- ^ Human-readable text message.
290 } deriving ( Show, Eq, Ord, Typeable, Data, Read )
291
292newtype TransactionId = TransactionId ByteString
293 deriving (Eq, Ord, Show, BEncode)
294
295newtype Method = Method ByteString
296 deriving (Eq, Ord, Show, BEncode)
297
298data Message a = Q { msgOrigin :: NodeId
299 , msgID :: TransactionId
300 , qryPayload :: a
301 , qryMethod :: Method
302 , qryReadOnly :: Bool }
303
304 | R { msgOrigin :: NodeId
305 , msgID :: TransactionId
306 , rspPayload :: Either Error a
307 , rspReflectedIP :: Maybe SockAddr }
308
309showBE :: BValue -> String
310showBE bval = L8.unpack (showBEncode bval)
311
312instance BE.BEncode (Message BValue) where
313 toBEncode m = encodeMessage m
314 {-
315 in case m of
316 Q {} -> trace ("encoded(query): "++showBE r) r
317 R {} -> trace ("encoded(response): "++showBE r) r -}
318 fromBEncode bval = decodeMessage bval
319 {-
320 in case r of
321 Left e -> trace (show e) r
322 Right (Q {}) -> trace ("decoded(query): "++showBE bval) r
323 Right (R {}) -> trace ("decoded(response): "++showBE bval) r -}
324
325decodeMessage :: BValue -> Either String (Message BValue)
326decodeMessage = fromDict $ do
327 key <- lookAhead (field (req "y"))
328 let _ = key :: BKey
329 f <- case key of
330 "q" -> do a <- field (req "a")
331 g <- either fail return $ flip fromDict a $ do
332 who <- field (req "id")
333 ro <- fromMaybe False <$> optional (field (req "ro"))
334 return $ \meth tid -> Q who tid a meth ro
335 meth <- field (req "q")
336 return $ g meth
337 "r" -> do ip <- do
338 ipstr <- optional (field (req "ip"))
339 mapM (either fail return . decodeAddr) ipstr
340 vals <- field (req "r")
341 either fail return $ flip fromDict vals $ do
342 who <- field (req "id")
343 return $ \tid -> R who tid (Right vals) ip
344 "e" -> do (ecode,emsg) <- field (req "e")
345 ip <- do
346 ipstr <- optional (field (req "ip"))
347 mapM (either fail return . decodeAddr) ipstr
348 -- FIXME:Spec does not give us the NodeId of the sender.
349 -- Using 'zeroID' as place holder.
350 -- We should ignore the msgOrigin for errors in 'updateRouting'.
351 -- We should consider making msgOrigin a Maybe value.
352 return $ \tid -> R zeroID tid (Left (Error ecode emsg)) ip
353 _ -> fail $ "Mainline message is not a query, response, or an error: "
354 ++ show key
355 tid <- field (req "t")
356 return $ f (tid :: TransactionId)
357
358
359encodeMessage :: Message BValue -> BValue
360encodeMessage (Q origin tid a meth ro)
361 = case a of
362 BDict args -> encodeQuery tid meth (BDict $ genericArgs origin ro `BE.union` args)
363 _ -> encodeQuery tid meth a -- XXX: Not really a valid query.
364encodeMessage (R origin tid v ip)
365 = case v of
366 Right (BDict vals) -> encodeResponse tid (BDict $ genericArgs origin False `BE.union` vals) ip
367 Left err -> encodeError tid err
368
369
370encodeAddr :: SockAddr -> ByteString
371encodeAddr = either encode4 encode6 . either4or6
372 where
373 encode4 (SockAddrInet port addr)
374 = S.runPut (S.putWord32host addr >> S.putWord16be (fromIntegral port))
375
376 encode6 (SockAddrInet6 port _ addr _)
377 = S.runPut (S.put addr >> S.putWord16be (fromIntegral port))
378 encode6 _ = B.empty
379
380decodeAddr :: ByteString -> Either String SockAddr
381decodeAddr bs = S.runGet g bs
382 where
383 g | (B.length bs == 6) = flip SockAddrInet <$> S.getWord32host <*> (fromIntegral <$> S.getWord16be)
384 | otherwise = do host <- S.get -- TODO: Is this right?
385 port <- fromIntegral <$> S.getWord16be
386 return $ SockAddrInet6 port 0 host 0
387
388genericArgs :: BEncode a => a -> Bool -> BDict
389genericArgs nodeid ro =
390 "id" .=! nodeid
391 .: "ro" .=? bool Nothing (Just (1 :: Int)) ro
392 .: endDict
393
394encodeError :: BEncode a => a -> Error -> BValue
395encodeError tid (Error ecode emsg) = encodeAny tid "e" (ecode,emsg) id
396
397encodeResponse :: (BEncode tid, BEncode vals) =>
398 tid -> vals -> Maybe SockAddr -> BValue
399encodeResponse tid rvals rip =
400 encodeAny tid "r" rvals ("ip" .=? (BString . encodeAddr <$> rip) .:)
401
402encodeQuery :: (BEncode args, BEncode tid, BEncode method) =>
403 tid -> method -> args -> BValue
404encodeQuery tid qmeth qargs = encodeAny tid "q" qmeth ("a" .=! qargs .:)
405
406encodeAny ::
407 (BEncode tid, BEncode a) =>
408 tid -> BKey -> a -> (BDict -> BDict) -> BValue
409encodeAny tid key val aux = toDict $
410 aux $ key .=! val
411 .: "t" .=! tid
412 .: "y" .=! key
413 .: endDict
414
415
416showPacket :: ([L8.ByteString] -> [L8.ByteString]) -> SockAddr -> L8.ByteString -> ByteString -> String
417showPacket f addr flow bs = L8.unpack $ L8.unlines es
418 where
419 es = map (L8.append prefix) (f $ L8.lines pp)
420
421 prefix = L8.pack (either show show $ either4or6 addr) <> flow
422
423 pp = either L8.pack showBEncode $ BE.decode bs
424
425-- Add detailed printouts for every packet.
426addVerbosity :: Transport err SockAddr ByteString -> Transport err SockAddr ByteString
427addVerbosity tr =
428 tr { awaitMessage = \kont -> awaitMessage tr $ \m -> do
429 forM_ m $ mapM_ $ \(msg,addr) -> do
430 hPutStrLn stderr (showPacket id addr " --> " msg)
431 kont m
432 , sendMessage = \addr msg -> do
433 hPutStrLn stderr (showPacket id addr " <-- " msg)
434 sendMessage tr addr msg
435 }
436
437
438showParseError :: ByteString -> SockAddr -> String -> String
439showParseError bs addr err = showPacket (L8.pack err :) addr " --> " bs
440
441parsePacket :: ByteString -> SockAddr -> Either String (Message BValue, NodeInfo)
442parsePacket bs addr = left (showParseError bs addr) $ do
443 pkt <- BE.decode bs
444 -- TODO: Error packets do not include a valid msgOrigin.
445 -- The BE.decode method is using 'zeroID' as a placeholder.
446 ni <- nodeInfo (msgOrigin pkt) addr
447 return (pkt, ni)
448
449encodePacket :: Message BValue -> NodeInfo -> (ByteString, SockAddr)
450encodePacket msg ni = ( toStrict $ BE.encode msg
451 , nodeAddr ni )
452
453classify :: Message BValue -> MessageClass String Method TransactionId NodeInfo (Message BValue)
454classify (Q { msgID = tid, qryMethod = meth }) = IsQuery meth tid
455classify (R { msgID = tid }) = IsResponse tid
456
457encodeResponsePayload :: BEncode a => TransactionId -> NodeInfo -> NodeInfo -> a -> Message BValue
458encodeResponsePayload tid self dest b = R (nodeId self) tid (Right $ BE.toBEncode b) (Just $ nodeAddr dest)
459
460encodeQueryPayload :: BEncode a =>
461 Method -> Bool -> TransactionId -> NodeInfo -> NodeInfo -> a -> Message BValue
462encodeQueryPayload meth isReadonly tid self dest b = Q (nodeId self) tid (BE.toBEncode b) meth isReadonly
463
464errorPayload :: TransactionId -> NodeInfo -> NodeInfo -> Error -> Message a
465errorPayload tid self dest e = R (nodeId self) tid (Left e) (Just $ nodeAddr dest)
466
467decodePayload :: BEncode a => Message BValue -> Either String a
468decodePayload msg = BE.fromBEncode $ qryPayload msg
469
470type Handler = MethodHandler String TransactionId NodeInfo (Message BValue)
471
472handler :: ( BEncode a
473 , BEncode b
474 ) =>
475 (NodeInfo -> a -> IO b) -> Maybe Handler
476handler f = Just $ MethodHandler decodePayload encodeResponsePayload f
477
478
479handlerE :: ( BEncode a
480 , BEncode b
481 ) =>
482 (NodeInfo -> a -> IO (Either Error b)) -> Maybe Handler
483handlerE f = Just $ MethodHandler decodePayload enc f
484 where
485 enc tid self dest (Left e) = errorPayload tid self dest e
486 enc tid self dest (Right b) = encodeResponsePayload tid self dest b
487
488type AnnounceSet = Set (InfoHash, PortNumber)
489
490data SwarmsDatabase = SwarmsDatabase
491 { contactInfo :: !( TVar PeerStore ) -- ^ Published by other nodes.
492 , sessionTokens :: !( TVar SessionTokens ) -- ^ Query session IDs.
493 , announceInfo :: !( TVar AnnounceSet ) -- ^ To publish by this node.
494 }
495
496newSwarmsDatabase :: IO SwarmsDatabase
497newSwarmsDatabase = do
498 toks <- nullSessionTokens
499 atomically
500 $ SwarmsDatabase <$> newTVar def
501 <*> newTVar toks
502 <*> newTVar def
503
504data Routing = Routing
505 { tentativeId :: NodeInfo
506 , committee4 :: TriadCommittee NodeId SockAddr
507 , committee6 :: TriadCommittee NodeId SockAddr
508 , refresher4 :: BucketRefresher NodeId NodeInfo
509 , refresher6 :: BucketRefresher NodeId NodeInfo
510 }
511
512sched4 :: Routing -> TVar (Int.PSQ POSIXTime)
513sched4 Routing { refresher4 = BucketRefresher { refreshQueue } } = refreshQueue
514
515sched6 :: Routing -> TVar (Int.PSQ POSIXTime)
516sched6 Routing { refresher6 = BucketRefresher { refreshQueue } } = refreshQueue
517
518routing4 :: Routing -> TVar (R.BucketList NodeInfo)
519routing4 Routing { refresher4 = BucketRefresher { refreshBuckets } } = refreshBuckets
520
521routing6 :: Routing -> TVar (R.BucketList NodeInfo)
522routing6 Routing { refresher6 = BucketRefresher { refreshBuckets } } = refreshBuckets
523
524traced :: Show tid => TableMethods t tid -> TableMethods t tid
525traced (TableMethods ins del lkup)
526 = TableMethods (\tid mvar t -> trace ("insert "++show tid) $ ins tid mvar t)
527 (\tid t -> trace ("del "++show tid) $ del tid t)
528 (\tid t -> trace ("lookup "++show tid) $ lkup tid t)
529
530
531type MainlineClient = Client String Method TransactionId NodeInfo (Message BValue)
532
533-- | Like 'nodeInfo' but falls back to 'iNADDR_ANY' for nodeIP' and 'nodePort'.
534mkNodeInfo :: NodeId -> SockAddr -> NodeInfo
535mkNodeInfo nid addr = NodeInfo
536 { nodeId = nid
537 , nodeIP = fromMaybe (toEnum 0) $ fromSockAddr addr
538 , nodePort = fromMaybe 0 $ sockAddrPort addr
539 }
540
541newClient :: SwarmsDatabase -> SockAddr
542 -> IO ( MainlineClient
543 , Routing
544 , [NodeInfo] -> [NodeInfo] -> IO ()
545 , [NodeInfo] -> [NodeInfo] -> IO ()
546 )
547newClient swarms addr = do
548 udp <- udpTransport addr
549 nid <- NodeId <$> getRandomBytes 20
550 let tentative_info = mkNodeInfo nid addr
551 tentative_info6 <-
552 maybe tentative_info
553 (\ip6 -> tentative_info { nodeId = fromMaybe (nodeId tentative_info)
554 $ bep42 (toSockAddr ip6) (nodeId tentative_info)
555 , nodeIP = IPv6 ip6
556 })
557 <$> global6
558 addr4 <- atomically $ newTChan
559 addr6 <- atomically $ newTChan
560 mkrouting <- atomically $ do
561 -- We defer initializing the refreshSearch and refreshPing until we
562 -- have a client to send queries with.
563 let nullPing = const $ return False
564 nullSearch = mainlineSearch $ \_ _ -> return Nothing
565 refresher4 <- newBucketRefresher tentative_info nullSearch nullPing
566 refresher6 <- newBucketRefresher tentative_info6 nullSearch nullPing
567 let tbl4 = refreshBuckets refresher4
568 tbl6 = refreshBuckets refresher6
569 updateIPVote tblvar addrvar a = do
570 bkts <- readTVar tblvar
571 case bep42 a (nodeId $ R.thisNode bkts) of
572 Just nid -> do
573 let tbl = R.nullTable (comparing nodeId)
574 (\s -> hashWithSalt s . nodeId)
575 (mkNodeInfo nid a)
576 (R.defaultBucketCount)
577 writeTVar tblvar tbl
578 writeTChan addrvar (a,map fst $ concat $ R.toList bkts)
579 Nothing -> return ()
580 committee4 <- newTriadCommittee $ updateIPVote tbl4 addr4
581 committee6 <- newTriadCommittee $ updateIPVote tbl6 addr6
582 return $ \client ->
583 -- Now we have a client, so tell the BucketRefresher how to search and ping.
584 let updIO r = updateRefresherIO (nodeSearch client) (ping client) r
585 in Routing tentative_info committee4 committee6 (updIO refresher4) (updIO refresher6)
586 map_var <- atomically $ newTVar (0, mempty)
587
588 let routing = mkrouting outgoingClient
589
590 net = onInbound (updateRouting outgoingClient routing)
591 $ layerTransport parsePacket encodePacket
592 $ udp
593
594 -- Paranoid: It's safe to define /net/ and /client/ to be mutually
595 -- recursive since 'updateRouting' does not invoke 'awaitMessage' which
596 -- which was modified by 'onInbound'. However, I'm going to avoid the
597 -- mutual reference just to be safe.
598 outgoingClient = client { clientNet = net { awaitMessage = ($ Nothing) } }
599
600 dispatch = DispatchMethods
601 { classifyInbound = classify -- :: x -> MessageClass err meth tid addr x
602 , lookupHandler = handlers -- :: meth -> Maybe (MethodHandler err tid addr x)
603 , tableMethods = mapT -- :: TransactionMethods tbl tid x
604 }
605
606 handlers :: Method -> Maybe Handler
607 handlers ( Method "ping" ) = handler pingH
608 handlers ( Method "find_node" ) = handler $ findNodeH routing
609 handlers ( Method "get_peers" ) = handler $ getPeersH routing swarms
610 handlers ( Method "announce_peer" ) = handlerE $ announceH swarms
611 handlers ( Method meth ) = Just $ defaultHandler meth
612
613 mapT = transactionMethods mapMethods gen
614
615 gen :: Word16 -> (TransactionId, Word16)
616 gen cnt = (TransactionId $ S.encode cnt, cnt+1)
617
618 ignoreParseError :: String -> IO ()
619 ignoreParseError _ = return ()
620
621 client = Client
622 { clientNet = addHandler ignoreParseError (handleMessage client) net
623 , clientDispatcher = dispatch
624 , clientErrorReporter = ignoreErrors -- printErrors stderr
625 , clientPending = map_var
626 , clientAddress = \maddr -> atomically $ do
627 let var = case flip prefer4or6 Nothing <$> maddr of
628 Just Want_IP6 -> routing6 routing
629 _ -> routing4 routing
630 R.thisNode <$> readTVar var
631 , clientResponseId = return
632 }
633
634 -- TODO: Provide some means of shutting down these four auxillary threads:
635
636 fork $ fix $ \again -> do
637 myThreadId >>= flip labelThread "addr4"
638 (addr, ns) <- atomically $ readTChan addr4
639 hPutStrLn stderr $ "External IPv4: "++show (addr, length ns)
640 forM_ ns $ \n -> do
641 hPutStrLn stderr $ "Change IP, ping: "++show n
642 ping outgoingClient n
643 -- TODO: trigger bootstrap ipv4
644 again
645 fork $ fix $ \again -> do
646 myThreadId >>= flip labelThread "addr6"
647 (addr,ns) <- atomically $ readTChan addr6
648 hPutStrLn stderr $ "External IPv6: "++show (addr, length ns)
649 forM_ ns $ \n -> do
650 hPutStrLn stderr $ "Change IP, ping: "++show n
651 ping outgoingClient n
652 -- TODO: trigger bootstrap ipv6
653 again
654
655
656 refresh_thread4 <- forkPollForRefresh $ refresher4 routing
657 refresh_thread6 <- forkPollForRefresh $ refresher6 routing
658
659 return (client, routing, bootstrap (refresher4 routing), bootstrap (refresher6 routing))
660
661-- | Modifies a purely random 'NodeId' to one that is related to a given
662-- routable address in accordance with BEP 42.
663--
664-- Test vectors from the spec:
665--
666-- IP rand example node ID
667-- ============ ===== ==========================================
668-- 124.31.75.21 1 5fbfbf f10c5d6a4ec8a88e4c6ab4c28b95eee4 01
669-- 21.75.31.124 86 5a3ce9 c14e7a08645677bbd1cfe7d8f956d532 56
670-- 65.23.51.170 22 a5d432 20bc8f112a3d426c84764f8c2a1150e6 16
671-- 84.124.73.14 65 1b0321 dd1bb1fe518101ceef99462b947a01ff 41
672-- 43.213.53.83 90 e56f6c bf5b7c4be0237986d5243b87aa6d5130 5a
673bep42 :: SockAddr -> NodeId -> Maybe NodeId
674bep42 addr0 (NodeId r)
675 | let addr = either id id $ either4or6 addr0 -- unmap 4mapped SockAddrs
676 , Just ip <- fmap S.encode (fromSockAddr addr :: Maybe IPv4)
677 <|> fmap S.encode (fromSockAddr addr :: Maybe IPv6)
678 = genBucketSample' retr (NodeId $ crc $ applyMask ip) (3,0x07,0)
679 | otherwise
680 = Nothing
681 where
682 ip4mask = "\x03\x0f\x3f\xff" :: ByteString
683 ip6mask = "\x01\x03\x07\x0f\x1f\x3f\x7f\xff" :: ByteString
684 nbhood_select = B.last r .&. 7
685 retr n = pure $ B.drop (B.length r - n) r
686 crc = S.encode . crc32c . B.pack
687 applyMask ip = case B.zipWith (.&.) msk ip of
688 (b:bs) -> (b .|. shiftL nbhood_select 5) : bs
689 bs -> bs
690 where msk | B.length ip == 4 = ip4mask
691 | otherwise = ip6mask
692
693
694
695defaultHandler :: ByteString -> Handler
696defaultHandler meth = MethodHandler decodePayload errorPayload returnError
697 where
698 returnError :: NodeInfo -> BValue -> IO Error
699 returnError _ _ = return $ Error MethodUnknown ("Unknown method " <> meth)
700
701mainlineKademlia :: MainlineClient
702 -> TriadCommittee NodeId SockAddr
703 -> BucketRefresher NodeId NodeInfo
704 -> Kademlia NodeId NodeInfo
705mainlineKademlia client committee refresher
706 = Kademlia quietInsertions
707 mainlineSpace
708 (vanillaIO (refreshBuckets refresher) $ ping client)
709 { tblTransition = \tr -> do
710 io1 <- transitionCommittee committee tr
711 io2 <- touchBucket refresher tr
712 return $ do
713 io1 >> io2
714 {- noisy (timestamp updates are currently reported as transitions to Accepted)
715 hPutStrLn stderr $ unwords
716 [ show (transitionedTo tr)
717 , show (transitioningNode tr)
718 ] -}
719 }
720
721
722mainlineSpace :: R.KademliaSpace NodeId NodeInfo
723mainlineSpace = R.KademliaSpace
724 { R.kademliaLocation = nodeId
725 , R.kademliaTestBit = testIdBit
726 , R.kademliaXor = xor
727 , R.kademliaSample = genBucketSample'
728 }
729
730transitionCommittee :: TriadCommittee NodeId SockAddr -> RoutingTransition NodeInfo -> STM (IO ())
731transitionCommittee committee (RoutingTransition ni Stranger) = do
732 delVote committee (nodeId ni)
733 return $ do
734 hPutStrLn stderr $ "delVote "++show (nodeId ni)
735transitionCommittee committee _ = return $ return ()
736
737updateRouting :: MainlineClient -> Routing -> NodeInfo -> Message BValue -> IO ()
738updateRouting client routing naddr msg = do
739 case prefer4or6 naddr Nothing of
740 Want_IP4 -> go (committee4 routing) (refresher4 routing)
741 Want_IP6 -> go (committee6 routing) (refresher6 routing)
742 where
743 go committee refresher = do
744 self <- atomically $ R.thisNode <$> readTVar (refreshBuckets refresher)
745 when (nodeIP self /= nodeIP naddr) $ do
746 case msg of
747 R { rspReflectedIP = Just sockaddr }
748 -> do
749 -- hPutStrLn stderr $ "External: "++show (nodeId naddr,sockaddr)
750 atomically $ addVote committee (nodeId naddr) sockaddr
751 _ -> return ()
752 insertNode (mainlineKademlia client committee refresher) naddr
753
754data Ping = Ping deriving Show
755
756-- Pong is the same as Ping.
757type Pong = Ping
758pattern Pong = Ping
759
760instance BEncode Ping where
761 toBEncode Ping = toDict endDict
762 fromBEncode _ = pure Ping
763
764wantList :: WantIP -> [ByteString]
765wantList Want_IP4 = ["ip4"]
766wantList Want_IP6 = ["ip6"]
767wantList Want_Both = ["ip4","ip6"]
768
769instance BEncode WantIP where
770 toBEncode w = toBEncode $ wantList w
771 fromBEncode bval = do
772 wants <- fromBEncode bval
773 let _ = wants :: [ByteString]
774 case (elem "ip4" wants, elem "ip6" wants) of
775 (True,True) -> Right Want_Both
776 (True,False) -> Right Want_IP4
777 (False,True) -> Right Want_IP6
778 _ -> Left "Unrecognized IP type."
779
780data FindNode = FindNode NodeId (Maybe WantIP)
781
782instance BEncode FindNode where
783 toBEncode (FindNode nid iptyp) = toDict $ target_key .=! nid
784 .: want_key .=? iptyp
785 .: endDict
786 fromBEncode = fromDict $ FindNode <$>! target_key
787 <*>? want_key
788
789data NodeFound = NodeFound
790 { nodes4 :: [NodeInfo]
791 , nodes6 :: [NodeInfo]
792 }
793
794instance BEncode NodeFound where
795 toBEncode (NodeFound ns ns6) = toDict $
796 nodes_key .=?
797 (if Prelude.null ns then Nothing
798 else Just (S.runPut (mapM_ putNodeInfo4 ns)))
799 .: nodes6_key .=?
800 (if Prelude.null ns6 then Nothing
801 else Just (S.runPut (mapM_ putNodeInfo6 ns6)))
802 .: endDict
803
804 fromBEncode bval = NodeFound <$> ns4 <*> ns6
805 where
806 opt ns = fromMaybe [] <$> optional ns
807 ns4 = opt $ fromDict (binary getNodeInfo4 nodes_key) bval
808 ns6 = opt $ fromDict (binary getNodeInfo6 nodes6_key) bval
809
810binary :: S.Get a -> BKey -> BE.Get [a]
811binary get k = field (req k) >>= either (fail . format) return .
812 S.runGet (many get)
813 where
814 format str = "fail to deserialize " ++ show k ++ " field: " ++ str
815
816pingH :: NodeInfo -> Ping -> IO Pong
817pingH _ Ping = return Pong
818
819prefer4or6 :: NodeInfo -> Maybe WantIP -> WantIP
820prefer4or6 addr iptyp = fromMaybe (ipFamily $ nodeIP addr) iptyp
821
822findNodeH :: Routing -> NodeInfo -> FindNode -> IO NodeFound
823findNodeH routing addr (FindNode node iptyp) = do
824 let preferred = prefer4or6 addr iptyp
825
826 (append4,append6) <- atomically $ do
827 ni4 <- R.thisNode <$> readTVar (routing4 routing)
828 ni6 <- R.thisNode <$> readTVar (routing6 routing)
829 return $ case ipFamily (nodeIP addr) of
830 Want_IP4 -> (id, (++ [ni6]))
831 Want_IP6 -> ((++ [ni4]), id)
832 ks <- bool (return []) (go append4 $ routing4 routing) (preferred /= Want_IP6)
833 ks6 <- bool (return []) (go append6 $ routing6 routing) (preferred /= Want_IP4)
834 return $ NodeFound ks ks6
835 where
836 go f var = f . R.kclosest mainlineSpace k node <$> atomically (readTVar var)
837
838 k = R.defaultK
839
840
841data GetPeers = GetPeers InfoHash (Maybe WantIP)
842
843instance BEncode GetPeers where
844 toBEncode (GetPeers ih iptyp)
845 = toDict $ info_hash_key .=! ih
846 .: want_key .=? iptyp
847 .: endDict
848 fromBEncode = fromDict $ GetPeers <$>! info_hash_key <*>? want_key
849
850
851data GotPeers = GotPeers
852 { -- | If the queried node has no peers for the infohash, returned
853 -- the K nodes in the queried nodes routing table closest to the
854 -- infohash supplied in the query.
855 peers :: [PeerAddr]
856
857 , nodes :: NodeFound
858
859 -- | The token value is a required argument for a future
860 -- announce_peer query.
861 , grantedToken :: Token
862 } -- deriving (Show, Eq, Typeable)
863
864nodeIsIPv6 :: NodeInfo -> Bool
865nodeIsIPv6 (NodeInfo _ (IPv6 _) _) = True
866nodeIsIPv6 _ = False
867
868instance BEncode GotPeers where
869 toBEncode GotPeers { nodes = NodeFound ns4 ns6, ..} = toDict $
870 nodes_key .=? (if null ns4 then Nothing
871 else Just $ S.runPut (mapM_ putNodeInfo4 ns4))
872 .: nodes6_key .=? (if null ns6 then Nothing
873 else Just $ S.runPut (mapM_ putNodeInfo4 ns6))
874 .: token_key .=! grantedToken
875 .: peers_key .=! map S.encode peers
876 .: endDict
877
878 fromBEncode = fromDict $ do
879 ns4 <- fromMaybe [] <$> optional (binary getNodeInfo4 nodes_key) -- "nodes"
880 ns6 <- fromMaybe [] <$> optional (binary getNodeInfo6 nodes6_key) -- "nodes6"
881 -- TODO: BEP 42...
882 --
883 -- Once enforced, responses to get_peers requests whose node ID does not
884 -- match its external IP should be considered to not contain a token and
885 -- thus not be eligible as storage target. Implementations should take
886 -- care that they find the closest set of nodes which return a token and
887 -- whose IDs matches their IPs before sending a store request to those
888 -- nodes.
889 --
890 -- Sounds like something to take care of at peer-search time, so I'll
891 -- ignore it for now.
892 tok <- field (req token_key) -- "token"
893 ps <- fromMaybe [] <$> optional (field (req peers_key) >>= decodePeers) -- "values"
894 pure $ GotPeers ps (NodeFound ns4 ns6) tok
895 where
896 decodePeers = either fail pure . mapM S.decode
897
898getPeersH :: Routing -> SwarmsDatabase -> NodeInfo -> GetPeers -> IO GotPeers
899getPeersH routing (SwarmsDatabase peers toks _) naddr (GetPeers ih iptyp) = do
900 ps <- do
901 tm <- getTimestamp
902 atomically $ do
903 (ps,store') <- Peers.freshPeers ih tm <$> readTVar peers
904 writeTVar peers store'
905 return ps
906 -- Filter peer results to only a single address family, IPv4 or IPv6, as
907 -- per BEP 32.
908 let notboth = iptyp >>= \case Want_Both -> Nothing
909 specific -> Just specific
910 selected = prefer4or6 naddr notboth
911 ps' = filter ( (== selected) . ipFamily . peerHost ) ps
912 tok <- grantToken toks naddr
913 ns <- findNodeH routing naddr (FindNode (coerce ih) iptyp)
914 return $ GotPeers ps' ns tok
915
916-- | Announce that the peer, controlling the querying node, is
917-- downloading a torrent on a port.
918data Announce = Announce
919 { -- | If set, the 'port' field should be ignored and the source
920 -- port of the UDP packet should be used as the peer's port
921 -- instead. This is useful for peers behind a NAT that may not
922 -- know their external port, and supporting uTP, they accept
923 -- incoming connections on the same port as the DHT port.
924 impliedPort :: Bool
925
926 -- | infohash of the torrent;
927 , topic :: InfoHash
928
929 -- | some clients announce the friendly name of the torrent here.
930 , announcedName :: Maybe ByteString
931
932 -- | the port /this/ peer is listening;
933 , port :: PortNumber
934
935 -- TODO: optional boolean "seed" key
936
937 -- | received in response to a previous get_peers query.
938 , sessionToken :: Token
939
940 } deriving (Show, Eq, Typeable)
941
942mkAnnounce :: PortNumber -> InfoHash -> Token -> Announce
943mkAnnounce portnum info token = Announce
944 { topic = info
945 , port = portnum
946 , sessionToken = token
947 , announcedName = Nothing
948 , impliedPort = False
949 }
950
951
952instance BEncode Announce where
953 toBEncode Announce {..} = toDict $
954 implied_port_key .=? flagField impliedPort
955 .: info_hash_key .=! topic
956 .: name_key .=? announcedName
957 .: port_key .=! port
958 .: token_key .=! sessionToken
959 .: endDict
960 where
961 flagField flag = if flag then Just (1 :: Int) else Nothing
962
963 fromBEncode = fromDict $ do
964 Announce <$> (boolField <$> optional (field (req implied_port_key)))
965 <*>! info_hash_key
966 <*>? name_key
967 <*>! port_key
968 <*>! token_key
969 where
970 boolField = maybe False (/= (0 :: Int))
971
972
973
974-- | The queried node must verify that the token was previously sent
975-- to the same IP address as the querying node. Then the queried node
976-- should store the IP address of the querying node and the supplied
977-- port number under the infohash in its store of peer contact
978-- information.
979data Announced = Announced
980 deriving (Show, Eq, Typeable)
981
982instance BEncode Announced where
983 toBEncode _ = toBEncode Ping
984 fromBEncode _ = pure Announced
985
986announceH :: SwarmsDatabase -> NodeInfo -> Announce -> IO (Either Error Announced)
987announceH (SwarmsDatabase peers toks _) naddr announcement = do
988 checkToken toks naddr (sessionToken announcement)
989 >>= bool (Left <$> return (Error ProtocolError "invalid parameter: token"))
990 (Right <$> go)
991 where
992 go = atomically $ do
993 modifyTVar' peers
994 $ insertPeer (topic announcement) (announcedName announcement)
995 $ PeerAddr
996 { peerId = Nothing
997 -- Avoid storing IPv4-mapped addresses.
998 , peerHost = case nodeIP naddr of
999 IPv6 ip6 | Just ip4 <- un4map ip6 -> IPv4 ip4
1000 a -> a
1001 , peerPort = if impliedPort announcement
1002 then nodePort naddr
1003 else port announcement
1004 }
1005 return Announced
1006
1007isReadonlyClient :: MainlineClient -> Bool
1008isReadonlyClient client = False -- TODO
1009
1010mainlineSend :: ( BEncode a
1011 , BEncode a2
1012 ) => Method
1013 -> (a2 -> b)
1014 -> (t -> a)
1015 -> MainlineClient
1016 -> t
1017 -> NodeInfo
1018 -> IO (Maybe b)
1019mainlineSend meth unwrap msg client nid addr = do
1020 reply <- sendQuery client serializer (msg nid) addr
1021 -- sendQuery will return (Just (Left _)) on a parse error. We're going to
1022 -- blow it away with the join-either sequence.
1023 -- TODO: Do something with parse errors.
1024 return $ join $ either (const Nothing) Just <$> reply
1025 where
1026 serializer = MethodSerializer
1027 { methodTimeout = \_ ni -> return (ni, 5000000)
1028 , method = meth
1029 , wrapQuery = encodeQueryPayload meth (isReadonlyClient client)
1030 , unwrapResponse = (>>= either (Left . Error GenericError . C8.pack)
1031 (Right . unwrap)
1032 . BE.fromBEncode)
1033 . rspPayload
1034 }
1035
1036ping :: MainlineClient -> NodeInfo -> IO Bool
1037ping client addr =
1038 fromMaybe False
1039 <$> mainlineSend (Method "ping") (\Pong -> True) (const Ping) client () addr
1040
1041-- searchQuery :: ni -> IO (Maybe [ni], [r], tok))
1042getNodes :: MainlineClient -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[NodeInfo],Maybe ()))
1043getNodes = mainlineSend (Method "find_node") unwrapNodes $ flip FindNode (Just Want_Both)
1044
1045unwrapNodes :: NodeFound -> ([NodeInfo], [NodeInfo], Maybe ())
1046unwrapNodes (NodeFound ns4 ns6) = (ns4++ns6, ns4++ns6, Just ())
1047
1048getPeers :: MainlineClient -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[PeerAddr],Maybe Token))
1049getPeers = mainlineSend (Method "get_peers") unwrapPeers $ flip GetPeers (Just Want_Both) . coerce
1050
1051unwrapPeers :: GotPeers -> ([NodeInfo], [PeerAddr], Maybe Token)
1052unwrapPeers (GotPeers ps (NodeFound ns4 ns6) tok) = (ns4++ns6, ps, Just tok)
1053
1054mainlineSearch :: (NodeId -> NodeInfo -> IO (Maybe ([NodeInfo], [r], Maybe tok)))
1055 -> Search NodeId (IP, PortNumber) tok NodeInfo r
1056mainlineSearch qry = Search
1057 { searchSpace = mainlineSpace
1058 , searchNodeAddress = nodeIP &&& nodePort
1059 , searchQuery = qry
1060 }
1061
1062nodeSearch :: MainlineClient -> Search NodeId (IP, PortNumber) () NodeInfo NodeInfo
1063nodeSearch client = mainlineSearch (getNodes client)
1064
1065peerSearch :: MainlineClient -> Search NodeId (IP, PortNumber) Token NodeInfo PeerAddr
1066peerSearch client = mainlineSearch (getPeers client)
1067
1068-- | List of bootstrap nodes maintained by different bittorrent
1069-- software authors.
1070bootstrapNodes :: WantIP -> IO [NodeInfo]
1071bootstrapNodes want = unsafeInterleaveIO $ do
1072 let wellknowns =
1073 [ "router.bittorrent.com:6881" -- by BitTorrent Inc.
1074
1075 -- doesn't work at the moment (use git blame) of commit
1076 , "dht.transmissionbt.com:6881" -- by Transmission project
1077
1078 , "router.utorrent.com:6881"
1079 ]
1080 nss <- forM wellknowns $ \hostAndPort -> do
1081 e <- resolve want hostAndPort
1082 case e of
1083 Left _ -> return []
1084 Right sockaddr -> either (const $ return [])
1085 (return . (: []))
1086 $ nodeInfo zeroID sockaddr
1087 return $ concat nss
1088
1089-- | Resolve either a numeric network address or a hostname to a
1090-- numeric IP address of the node.
1091resolve :: WantIP -> String -> IO (Either IOError SockAddr)
1092resolve want hostAndPort = do
1093 let hints = defaultHints { addrSocketType = Datagram
1094 , addrFamily = case want of
1095 Want_IP4 -> AF_INET
1096 _ -> AF_INET6
1097 }
1098 (rport,rhost) = span (/= ':') $ reverse hostAndPort
1099 (host,port) = case rhost of
1100 [] -> (hostAndPort, Nothing)
1101 (_:hs) -> (reverse hs, Just (reverse rport))
1102 tryIOError $ do
1103 -- getAddrInfo throws exception on empty list, so this
1104 -- pattern matching never fails.
1105 info : _ <- getAddrInfo (Just hints) (Just host) port
1106 return $ addrAddress info
1107
1108
1109announce :: MainlineClient -> Announce -> NodeInfo -> IO (Maybe Announced)
1110announce client msg addr = do
1111 mainlineSend (Method "announce_peer") id (\() -> msg) client () addr
diff --git a/src/Network/BitTorrent/MainlineDHT/Symbols.hs b/src/Network/BitTorrent/MainlineDHT/Symbols.hs
new file mode 100644
index 00000000..05a64014
--- /dev/null
+++ b/src/Network/BitTorrent/MainlineDHT/Symbols.hs
@@ -0,0 +1,24 @@
1{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
2module Network.BitTorrent.MainlineDHT.Symbols where
3
4import Data.BEncode.BDict
5
6peer_ip_key = "ip" :: BKey
7peer_id_key = "peer id" :: BKey
8peer_port_key = "port" :: BKey
9msg_type_key = "msg_type" :: BKey
10piece_key = "piece" :: BKey
11total_size_key = "total_size" :: BKey
12node_id_key = "id" :: BKey
13read_only_key = "ro" :: BKey
14want_key = "want" :: BKey
15target_key = "target" :: BKey
16nodes_key = "nodes" :: BKey
17nodes6_key = "nodes6" :: BKey
18info_hash_key = "info_hash" :: BKey
19peers_key = "values" :: BKey
20token_key = "token" :: BKey
21name_key = "name" :: BKey
22port_key = "port" :: BKey
23implied_port_key = "implied_port" :: BKey
24
diff --git a/src/Network/Kademlia.hs b/src/Network/Kademlia.hs
new file mode 100644
index 00000000..8956df2c
--- /dev/null
+++ b/src/Network/Kademlia.hs
@@ -0,0 +1,180 @@
1{-# LANGUAGE CPP, ScopedTypeVariables, PartialTypeSignatures, FlexibleContexts #-}
2{-# LANGUAGE KindSignatures #-}
3{-# LANGUAGE DeriveFunctor, DeriveTraversable #-}
4-- {-# LANGUAGE TypeFamilies #-}
5{-# LANGUAGE GADTs #-}
6{-# LANGUAGE PatternSynonyms #-}
7module Network.Kademlia where
8
9import Data.Function
10import Data.Maybe
11import qualified Data.Set as Set
12import Data.Time.Clock (getCurrentTime)
13import Data.Time.Clock.POSIX (getPOSIXTime, utcTimeToPOSIXSeconds)
14import Network.Kademlia.Routing as R
15#ifdef THREAD_DEBUG
16import Control.Concurrent.Lifted.Instrument
17#else
18import Control.Concurrent.Lifted
19import GHC.Conc (labelThread)
20#endif
21import Control.Concurrent.STM
22import Control.Monad
23import Data.Bits
24import Data.Hashable
25import Data.IP
26import Data.Monoid
27import Data.Serialize (Serialize)
28import Data.Time.Clock.POSIX (POSIXTime)
29import qualified Data.Wrapper.PSQInt as Int
30 ;import Data.Wrapper.PSQInt (pattern (:->))
31import Network.Address (bucketRange,genBucketSample)
32import Network.Kademlia.Search
33import System.Entropy
34import System.Timeout
35import Text.PrettyPrint as PP hiding (($$), (<>))
36import Text.PrettyPrint.HughesPJClass hiding (($$), (<>))
37import System.IO
38import Control.Concurrent.Tasks
39
40-- | The status of a given node with respect to a given routint table.
41data RoutingStatus
42 = Stranger -- ^ The node is unknown to the Kademlia routing table.
43 | Applicant -- ^ The node may be inserted pending a ping timeout.
44 | Accepted -- ^ The node has a slot in one of the Kademlia buckets.
45 deriving (Eq,Ord,Enum,Show,Read)
46
47-- | A change occured in the kademlia routing table.
48data RoutingTransition ni = RoutingTransition
49 { transitioningNode :: ni
50 , transitionedTo :: !RoutingStatus
51 }
52 deriving (Eq,Ord,Show,Read)
53
54data InsertionReporter ni = InsertionReporter
55 { -- | Called on every inbound packet. Accepts:
56 --
57 -- * Origin of packet.
58 --
59 -- * List of nodes to be pinged as a result.
60 reportArrival :: POSIXTime
61 -> ni
62 -> [ni]
63 -> IO ()
64 -- | Called on every ping probe. Accepts:
65 --
66 -- * Who was pinged.
67 --
68 -- * True Bool value if they ponged.
69 , reportPingResult :: POSIXTime
70 -> ni
71 -> Bool
72 -> IO ()
73 }
74
75quietInsertions :: InsertionReporter ni
76quietInsertions = InsertionReporter
77 { reportArrival = \_ _ _ -> return ()
78 , reportPingResult = \_ _ _ -> return ()
79 }
80
81contramapIR :: (t -> ni) -> InsertionReporter ni -> InsertionReporter t
82contramapIR f ir = InsertionReporter
83 { reportArrival = \tm ni nis -> reportArrival ir tm (f ni) (map f nis)
84 , reportPingResult = \tm ni b -> reportPingResult ir tm (f ni) b
85 }
86
87-- | All the IO operations neccessary to maintain a Kademlia routing table.
88data TableStateIO ni = TableStateIO
89 { -- | Write the routing table. Typically 'writeTVar'.
90 tblWrite :: R.BucketList ni -> STM ()
91
92 -- | Read the routing table. Typically 'readTVar'.
93 , tblRead :: STM (R.BucketList ni)
94
95 -- | Issue a ping to a remote node and report 'True' if the node
96 -- responded within an acceptable time and 'False' otherwise.
97 , tblPing :: ni -> IO Bool
98
99 -- | Convenience method provided to assist in maintaining state
100 -- consistent with the routing table. It will be invoked in the same
101 -- transaction that 'tblRead'\/'tblWrite' occured but only when there was
102 -- an interesting change. The returned IO action will be triggered soon
103 -- afterward.
104 --
105 -- It is not necessary to do anything interesting here. The following
106 -- trivial implementation is fine:
107 --
108 -- > tblTransition = const $ return $ return ()
109 , tblTransition :: RoutingTransition ni -> STM (IO ())
110 }
111
112vanillaIO :: TVar (BucketList ni) -> (ni -> IO Bool) -> TableStateIO ni
113vanillaIO var ping = TableStateIO
114 { tblRead = readTVar var
115 , tblWrite = writeTVar var
116 , tblPing = ping
117 , tblTransition = const $ return $ return ()
118 }
119
120-- | Everything neccessary to maintain a routing table of /ni/ (node
121-- information) entries.
122data Kademlia nid ni = Kademlia (InsertionReporter ni)
123 (KademliaSpace nid ni)
124 (TableStateIO ni)
125
126
127-- Helper to 'insertNode'.
128--
129-- Adapt return value from 'updateForPingResult' into a
130-- more easily groked list of transitions.
131transition :: (ni,Maybe (t,ni)) -> [RoutingTransition ni]
132transition (x,m) =
133 -- Just _ <- m = Node transition: Accepted --> Stranger
134 -- Nothing <- m = Node transition: Applicant --> Stranger
135 RoutingTransition x Stranger
136 : maybeToList (accepted <$> m)
137
138-- Helper to 'transition'
139--
140-- Node transition: Applicant --> Accepted
141accepted :: (t,ni) -> RoutingTransition ni
142accepted (_,y) = RoutingTransition y Accepted
143
144
145insertNode :: Kademlia nid ni -> ni -> IO ()
146insertNode (Kademlia reporter space io) node = do
147
148 tm <- utcTimeToPOSIXSeconds <$> getCurrentTime
149
150 (ps,reaction) <- atomically $ do
151 tbl <- tblRead io
152 let (inserted, ps,t') = R.updateForInbound space tm node tbl
153 tblWrite io t'
154 reaction <- case ps of
155 _ | inserted -> -- Node transition: Stranger --> Accepted
156 tblTransition io $ RoutingTransition node Accepted
157 (_:_) -> -- Node transition: Stranger --> Applicant
158 tblTransition io $ RoutingTransition node Applicant
159 _ -> return $ return ()
160 return (ps, reaction)
161
162 reportArrival reporter tm node ps
163 reaction
164
165 _ <- fork $ do
166 myThreadId >>= flip labelThread "pingResults"
167 forM_ ps $ \n -> do
168 b <- tblPing io n
169 reportPingResult reporter tm n b -- XXX: tm is timestamp of original triggering packet, not result
170 join $ atomically $ do
171 tbl <- tblRead io
172 let (replacements, t') = R.updateForPingResult space n b tbl
173 tblWrite io t'
174 ios <- sequence $ concatMap
175 (map (tblTransition io) . transition)
176 replacements
177 return $ sequence_ ios
178
179 return ()
180
diff --git a/src/Network/Kademlia/Bootstrap.hs b/src/Network/Kademlia/Bootstrap.hs
new file mode 100644
index 00000000..93cf08f3
--- /dev/null
+++ b/src/Network/Kademlia/Bootstrap.hs
@@ -0,0 +1,432 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE ConstraintKinds #-}
3{-# LANGUAGE DeriveFunctor #-}
4{-# LANGUAGE DeriveTraversable #-}
5{-# LANGUAGE FlexibleContexts #-}
6{-# LANGUAGE GADTs #-}
7{-# LANGUAGE KindSignatures #-}
8{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE NamedFieldPuns #-}
10{-# LANGUAGE PartialTypeSignatures #-}
11{-# LANGUAGE PatternSynonyms #-}
12{-# LANGUAGE RankNTypes #-}
13{-# LANGUAGE ScopedTypeVariables #-}
14module Network.Kademlia.Bootstrap where
15
16import Data.Function
17import Data.Maybe
18import qualified Data.Set as Set
19import Data.Time.Clock (getCurrentTime)
20import Data.Time.Clock.POSIX (getPOSIXTime, utcTimeToPOSIXSeconds)
21import Network.Kademlia.Routing as R
22#ifdef THREAD_DEBUG
23import Control.Concurrent.Lifted.Instrument
24#else
25import Control.Concurrent.Lifted
26import GHC.Conc (labelThread)
27#endif
28import Control.Concurrent.STM
29import Control.Monad
30import Data.Bits
31import Data.Hashable
32import Data.IP
33import Data.Monoid
34import Data.Serialize (Serialize)
35import Data.Time.Clock.POSIX (POSIXTime)
36import Data.Ord
37import System.Entropy
38import System.Timeout
39import Text.PrettyPrint as PP hiding (($$), (<>))
40import Text.PrettyPrint.HughesPJClass hiding (($$), (<>))
41import System.IO
42
43import qualified Data.Wrapper.PSQInt as Int
44 ;import Data.Wrapper.PSQInt (pattern (:->))
45import Network.Address (bucketRange,genBucketSample)
46import Network.Kademlia.Search
47import Control.Concurrent.Tasks
48import Network.Kademlia
49
50type SensibleNodeId nid ni =
51 ( Show nid
52 , Ord nid
53 , Ord ni
54 , Hashable nid
55 , Hashable ni )
56
57data BucketRefresher nid ni = forall tok addr. Ord addr => BucketRefresher
58 { -- | A staleness threshold (if a bucket goes this long without being
59 -- touched, a refresh will be triggered).
60 refreshInterval :: POSIXTime
61 -- | A TVar with the time-to-refresh schedule for each bucket.
62 --
63 -- To "touch" a bucket and prevent it from being refreshed, reschedule
64 -- its refresh time to some time into the future by modifying its
65 -- priority in this priority search queue.
66 , refreshQueue :: TVar (Int.PSQ POSIXTime)
67 -- | This is the kademlia node search specification.
68 , refreshSearch :: Search nid addr tok ni ni
69 -- | The current kademlia routing table buckets.
70 , refreshBuckets :: TVar (R.BucketList ni)
71 -- | Action to ping a node. This is used only during initial bootstrap
72 -- to get some nodes in our table. A 'True' result is interpreted as a a
73 -- pong, where 'False' is a non-response.
74 , refreshPing :: ni -> IO Bool
75 , -- | Timestamp of last bucket event.
76 refreshLastTouch :: TVar POSIXTime
77 , -- | This variable indicates whether or not we are in bootstrapping mode.
78 bootstrapMode :: TVar Bool
79 , -- | When this countdown reaches 0, we exit bootstrap mode. It is decremented on
80 -- every finished refresh.
81 bootstrapCountdown :: TVar (Maybe Int)
82 }
83
84newBucketRefresher :: ( Ord addr, Hashable addr
85 , SensibleNodeId nid ni )
86 => ni
87 -> Search nid addr tok ni ni
88 -> (ni -> IO Bool)
89 -> STM (BucketRefresher nid ni)
90newBucketRefresher template_ni sch ping = do
91 let spc = searchSpace sch
92 nodeId = kademliaLocation spc
93 bkts <- newTVar $ R.nullTable (comparing nodeId) (\s -> hashWithSalt s . nodeId) template_ni R.defaultBucketCount
94 sched <- newTVar Int.empty
95 lasttouch <- newTVar 0 -- Would use getPOSIXTime here, or minBound, but alas...
96 bootstrapVar <- newTVar True -- Start in bootstrapping mode.
97 bootstrapCnt <- newTVar Nothing
98 return BucketRefresher
99 { refreshInterval = 15 * 60
100 , refreshQueue = sched
101 , refreshSearch = sch
102 , refreshBuckets = bkts
103 , refreshPing = ping
104 , refreshLastTouch = lasttouch
105 , bootstrapMode = bootstrapVar
106 , bootstrapCountdown = bootstrapCnt
107 }
108
109-- | This was added to avoid the compile error "Record update for
110-- insufficiently polymorphic field" when trying to update the existentially
111-- quantified field 'refreshSearch'.
112updateRefresherIO :: Ord addr
113 => Search nid addr tok ni ni
114 -> (ni -> IO Bool)
115 -> BucketRefresher nid ni -> BucketRefresher nid ni
116updateRefresherIO sch ping BucketRefresher{..} = BucketRefresher
117 { refreshSearch = sch
118 , refreshPing = ping
119 , refreshInterval = refreshInterval
120 , refreshBuckets = refreshBuckets
121 , refreshQueue = refreshQueue
122 , refreshLastTouch = refreshLastTouch
123 , bootstrapMode = bootstrapMode
124 , bootstrapCountdown = bootstrapCountdown
125 }
126
127-- | Fork a refresh loop. Kill the returned thread to terminate it.
128forkPollForRefresh :: SensibleNodeId nid ni => BucketRefresher nid ni -> IO ThreadId
129forkPollForRefresh r@BucketRefresher{ refreshInterval
130 , refreshQueue
131 , refreshBuckets
132 , refreshSearch } = fork $ do
133 myThreadId >>= flip labelThread "pollForRefresh"
134 fix $ \again -> do
135 join $ atomically $ do
136 nextup <- Int.findMin <$> readTVar refreshQueue
137 maybe retry (return . go again) nextup
138 where
139 refresh :: Int -> IO Int
140 refresh n = do
141 -- hPutStrLn stderr $ "Refresh time! "++ show n
142 refreshBucket r n
143
144 go again ( bktnum :-> refresh_time ) = do
145 now <- getPOSIXTime
146 case fromEnum (refresh_time - now) of
147 x | x <= 0 -> do -- Refresh time!
148 -- Move it to the back of the refresh queue.
149 atomically $ do
150 interval <- effectiveRefreshInterval r bktnum
151 modifyTVar' refreshQueue
152 $ Int.insert bktnum (now + interval)
153 -- Now fork the refresh operation.
154 -- TODO: We should probably propogate the kill signal to this thread.
155 fork $ do myThreadId >>= flip labelThread ("refresh."++show bktnum)
156 _ <- refresh bktnum
157 return ()
158 return ()
159 picoseconds -> do
160 -- hPutStrLn stderr $ show (picoseconds `div` 10^12) ++ " seconds until refresh " ++ show bktnum
161 threadDelay ( picoseconds `div` 10^6 )
162 again
163
164
165-- | This is a helper to 'refreshBucket' which does some book keeping to decide
166-- whether or not a bucket is sufficiently refreshed or not. It will return
167-- false when we can terminate a node search.
168checkBucketFull :: Ord ni => KademliaSpace nid ni -- ^ Obtain a node id from a node.
169 -> TVar (BucketList ni) -- ^ The current routing table.
170 -> TVar (Set.Set ni) -- ^ In-range nodes found so far.
171 -> TVar Bool -- ^ The result will also be written here.
172 -> Int -- ^ The bucket number of interest.
173 -> ni -- ^ A newly found node.
174 -> STM Bool
175checkBucketFull space var resultCounter fin n found_node = do
176 let fullcount = R.defaultBucketSize
177 saveit True = writeTVar fin True >> return True
178 saveit _ = return False
179 tbl <- readTVar var
180 let counts = R.shape tbl
181 nid = kademliaLocation space found_node
182 -- Update the result set with every found node that is in the
183 -- bucket of interest.
184 when (n == R.bucketNumber space nid tbl)
185 $ modifyTVar' resultCounter (Set.insert found_node)
186 resultCount <- readTVar resultCounter
187 saveit $ case drop (n - 1) counts of
188 (cnt:_) | cnt < fullcount -> True -- bucket not full, keep going
189 _ | Set.size resultCount < fullcount -> True -- we haven't got many results, keep going
190 _ -> False -- okay, good enough, let's quit.
191
192-- | Called from 'refreshBucket' with the current time when a refresh of the
193-- supplied bucket number finishes.
194onFinishedRefresh :: BucketRefresher nid ni -> Int -> POSIXTime -> STM (IO ())
195onFinishedRefresh BucketRefresher { bootstrapCountdown
196 , bootstrapMode
197 , refreshQueue
198 , refreshBuckets } num now = do
199 bootstrapping <- readTVar bootstrapMode
200 if not bootstrapping then return $ return () -- hPutStrLn stderr $ "Finished non-boostrapping refresh: "++show num
201 else do
202 tbl <- readTVar refreshBuckets
203 action <-
204 if num /= R.bktCount tbl - 1
205 then do modifyTVar' bootstrapCountdown (fmap pred)
206 return $ return () -- hPutStrLn stderr $ "BOOTSTRAP decrement"
207 else do
208 -- The last bucket finished.
209 cnt <- readTVar bootstrapCountdown
210 case cnt of
211 Nothing -> do
212 let fullsize = R.defaultBucketSize
213 notfull (n,len) | n==num = False
214 | len>=fullsize = False
215 | otherwise = True
216 unfull = case filter notfull $ zip [0..] (R.shape tbl) of
217 [] -> [(0,0)] -- Schedule at least 1 more refresh.
218 xs -> xs
219 forM_ unfull $ \(n,_) -> do
220 -- Schedule immediate refresh for unfull buckets (other than this one).
221 modifyTVar' refreshQueue $ Int.insert n (now - 1)
222 writeTVar bootstrapCountdown $! Just $! length unfull
223 return $ return () -- hPutStrLn stderr $ "BOOTSTRAP scheduling: "++show unfull
224 Just n -> do writeTVar bootstrapCountdown $! Just $! pred n
225 return $ return () -- hPutStrLn stderr "BOOTSTRAP decrement (last bucket)"
226 cnt <- readTVar bootstrapCountdown
227 if (cnt == Just 0)
228 then do
229 -- Boostrap finished!
230 writeTVar bootstrapMode False
231 writeTVar bootstrapCountdown Nothing
232 return $ do action ; hPutStrLn stderr $ "BOOTSTRAP complete (" ++ show (R.shape tbl) ++ ")."
233 else return $ do action ; hPutStrLn stderr $ "BOOTSTRAP progress " ++ show (num,R.shape tbl,cnt)
234
235refreshBucket :: (Show nid, Ord ni, Ord nid, Hashable nid, Hashable ni) =>
236 BucketRefresher nid ni -> Int -> IO Int
237refreshBucket r@BucketRefresher{ refreshSearch = sch
238 , refreshBuckets = var }
239 n = do
240 tbl <- atomically (readTVar var)
241 let count = bktCount tbl
242 nid = kademliaLocation (searchSpace sch) (thisNode tbl)
243 sample <- if n+1 >= count -- Is this the last bucket?
244 then return nid -- Yes? Search our own id.
245 else kademliaSample (searchSpace sch) -- No? Generate a random id.
246 getEntropy
247 nid
248 (bucketRange n (n + 1 < count))
249 fin <- atomically $ newTVar False
250 resultCounter <- atomically $ newTVar Set.empty
251
252 hPutStrLn stderr $ "Start refresh " ++ show (n,sample)
253
254 -- Set 15 minute timeout in order to avoid overlapping refreshes.
255 s <- search sch tbl sample $ if n+1 == R.defaultBucketCount
256 then const $ return True -- Never short-circuit the last bucket.
257 else checkBucketFull (searchSpace sch) var resultCounter fin n
258 _ <- timeout (15*60*1000000) $ do
259 atomically $ searchIsFinished s >>= check
260 atomically $ searchCancel s
261 hPutStrLn stderr $ "Finish refresh " ++ show (n,sample)
262 now <- getPOSIXTime
263 join $ atomically $ onFinishedRefresh r n now
264 rcount <- atomically $ do
265 c <- Set.size <$> readTVar resultCounter
266 b <- readTVar fin
267 return $ if b then 1 else c
268 return rcount
269
270refreshLastBucket :: SensibleNodeId nid ni => BucketRefresher nid ni -> IO ()
271refreshLastBucket r@BucketRefresher { refreshBuckets
272 , refreshQueue } = do
273
274 now <- getPOSIXTime
275 atomically $ do
276 cnt <- bktCount <$> readTVar refreshBuckets
277 -- Schedule immediate refresh.
278 modifyTVar' refreshQueue $ Int.insert (cnt-1) (now - 1)
279
280restartBootstrap :: (Hashable ni, Hashable nid, Ord ni, Ord nid, Show nid) =>
281 BucketRefresher nid ni -> STM (IO ())
282restartBootstrap r@BucketRefresher{ bootstrapMode, bootstrapCountdown } = do
283 unchanged <- readTVar bootstrapMode
284 writeTVar bootstrapMode True
285 writeTVar bootstrapCountdown Nothing
286 if not unchanged then return $ do
287 hPutStrLn stderr "BOOTSTRAP entered bootstrap mode"
288 refreshLastBucket r
289 else return $ hPutStrLn stderr "BOOTSTRAP already bootstrapping"
290
291bootstrap :: (Ord ni, Ord nid, Hashable nid, Hashable ni, Foldable t, Foldable t1, Show nid) =>
292 BucketRefresher nid ni
293 -> t1 ni -- ^ Nodes to bootstrap from.
294 -> t ni -- ^ Fallback nodes; used only if the others are unresponsive.
295 -> IO ()
296bootstrap r@BucketRefresher { refreshSearch = sch
297 , refreshBuckets = var
298 , refreshPing = ping
299 , bootstrapMode } ns ns0 = do
300 gotPing <- atomically $ newTVar False
301
302 -- First, ping the given nodes so that they are added to
303 -- our routing table.
304 withTaskGroup "bootstrap.resume" 20 $ \g -> do
305 forM_ ns $ \n -> do
306 let lbl = show $ kademliaLocation (searchSpace sch) n
307 forkTask g lbl $ do
308 b <- ping n
309 when b $ atomically $ writeTVar gotPing True
310
311 -- We resort to the hardcoded fallback nodes only when we got no
312 -- responses. This is to lesson the burden on well-known boostrap
313 -- nodes.
314 fallback <- atomically (readTVar gotPing) >>= return . when . not
315 fallback $ withTaskGroup "bootstrap.ping" 20 $ \g -> do
316 forM_ ns0 $ \n -> do
317 forkTask g (show $ kademliaLocation (searchSpace sch) n)
318 (void $ ping n)
319 hPutStrLn stderr "Finished bootstrap pings."
320 -- Now search our own Id by entering bootstrap mode from non-bootstrap mode.
321 join $ atomically $ do
322 writeTVar bootstrapMode False
323 restartBootstrap r
324 --
325 -- Hopefully 'forkPollForRefresh' was invoked and can take over
326 -- maintenance.
327
328
329effectiveRefreshInterval :: BucketRefresher nid ni -> Int -> STM POSIXTime
330effectiveRefreshInterval BucketRefresher{ refreshInterval
331 , refreshBuckets
332 , bootstrapMode } num = do
333 tbl <- readTVar refreshBuckets
334 bootstrapping <- readTVar bootstrapMode
335 case bootstrapping of
336 False -> return refreshInterval
337 True -> do
338 -- When bootstrapping, refresh interval for non-full buckets is only 15 seconds.
339 let fullcount = R.defaultBucketSize
340 count = fromMaybe fullcount $ listToMaybe $ drop (num - 1) $ R.shape tbl
341 if count == fullcount
342 then return refreshInterval
343 else return 15 -- seconds
344
345
346
347-- | Reschedule a bucket's refresh-time. It should be called whenever a bucket
348-- changes. This will typically be invoked from 'tblTransition'.
349--
350-- From BEP 05:
351--
352-- > Each bucket should maintain a "last changed" property to indicate how
353-- > "fresh" the contents are.
354--
355-- We will use a "time to next refresh" property instead and store it in
356-- a priority search queue.
357--
358-- In detail using an expository (not actually implemented) type
359-- 'BucketTouchEvent'...
360--
361-- >>> data BucketTouchEvent = RoutingStatus :--> RoutingStatus
362-- >>> bucketEvents =
363-- >>> [ Applicant :--> Stranger -- a node in a bucket is pinged and it responds,
364-- >>>
365-- >>> , Stranger :--> Accepted -- or a node is added to a bucket,
366-- >>>
367-- >>> , Accepted :--> Stranger -- or a node in a bucket is replaced
368-- >>> , Applicant :--> Accepted -- with another node,
369-- >>> ]
370--
371-- the bucket's last changed property should be updated. Buckets that have not
372-- been changed in 15 minutes (see 'refreshInterval') should be "refreshed."
373-- This is done by picking a random ID in the range of the bucket and
374-- performing a find_nodes search on it.
375--
376-- The only other possible BucketTouchEvents are as follows:
377--
378-- >>> not_handled =
379-- >>> , Stranger :--> Applicant -- A ping is pending, it's result is covered:
380-- >>> -- (Applicant :--> Stranger)
381-- >>> -- (Applicant :--> Accepted)
382-- >>> , Accepted :--> Applicant -- Never happens
383-- >>> ]
384--
385-- Because this BucketTouchEvent type is not actually implemented and we only
386-- receive notifications of a node's new state, it suffices to reschedule the
387-- bucket refresh 'touchBucket' on every transition to a state other than
388-- 'Applicant'.
389--
390-- XXX: Unfortunately, this means redundantly triggering twice upon every node
391-- replacement because we do not currently distinguish between standalone
392-- insertion/deletion events and an insertion/deletion pair constituting
393-- replacement.
394--
395-- It might also be better to pass the timestamp of the transition here and
396-- keep the refresh queue in better sync with the routing table by updating it
397-- within the STM monad.
398--
399-- We embed the result in the STM monad but currently, no STM state changes
400-- occur until the returned IO action is invoked. TODO: simplify?
401touchBucket :: SensibleNodeId nid ni
402 => BucketRefresher nid ni
403 -> RoutingTransition ni -- ^ What happened to the bucket?
404 -> STM (IO ())
405touchBucket r@BucketRefresher{ refreshSearch
406 , refreshInterval
407 , refreshBuckets
408 , refreshQueue
409 , refreshLastTouch
410 , bootstrapMode
411 , bootstrapCountdown }
412 RoutingTransition{ transitionedTo
413 , transitioningNode }
414 = case transitionedTo of
415 Applicant -> return $ return () -- Ignore transition to applicant.
416 _ -> return $ do -- Reschedule for any other transition.
417 now <- getPOSIXTime
418 join $ atomically $ do
419 let space = searchSpace refreshSearch
420 nid = kademliaLocation space transitioningNode
421 tbl <- readTVar refreshBuckets
422 let num = R.bucketNumber space nid tbl
423 stamp <- readTVar refreshLastTouch
424 action <- case stamp /= 0 && (now - stamp > 60) of
425 True -> do
426 -- It's been one minute since any bucket has been touched, re-enter bootstrap mode.
427 restartBootstrap r
428 False -> return $ return ()
429 interval <- effectiveRefreshInterval r num
430 modifyTVar' refreshQueue $ Int.insert num (now + interval)
431 writeTVar refreshLastTouch now
432 return action
diff --git a/src/Network/Kademlia/Routing.hs b/src/Network/Kademlia/Routing.hs
new file mode 100644
index 00000000..a52cca73
--- /dev/null
+++ b/src/Network/Kademlia/Routing.hs
@@ -0,0 +1,808 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- (c) Joe Crayne 2017
4-- License : BSD3
5-- Maintainer : pxqr.sta@gmail.com
6-- Stability : experimental
7-- Portability : portable
8--
9-- Every node maintains a routing table of known good nodes. The
10-- nodes in the routing table are used as starting points for
11-- queries in the DHT. Nodes from the routing table are returned in
12-- response to queries from other nodes.
13--
14-- For more info see:
15-- <http://www.bittorrent.org/beps/bep_0005.html#routing-table>
16--
17{-# LANGUAGE CPP #-}
18{-# LANGUAGE RecordWildCards #-}
19{-# LANGUAGE BangPatterns #-}
20{-# LANGUAGE RankNTypes #-}
21{-# LANGUAGE ViewPatterns #-}
22{-# LANGUAGE TypeOperators #-}
23{-# LANGUAGE DeriveGeneric #-}
24{-# LANGUAGE DeriveFunctor #-}
25{-# LANGUAGE GADTs #-}
26{-# LANGUAGE ScopedTypeVariables #-}
27{-# LANGUAGE TupleSections #-}
28{-# LANGUAGE StandaloneDeriving, FlexibleContexts, MultiParamTypeClasses, FlexibleInstances #-}
29{-# OPTIONS_GHC -fno-warn-orphans #-}
30module Network.Kademlia.Routing
31 {-
32 ( -- * BucketList
33 BucketList
34 , Info(..)
35
36 -- * Attributes
37 , BucketCount
38 , defaultBucketCount
39 , BucketSize
40 , defaultBucketSize
41 , NodeCount
42
43 -- * Query
44 , Network.Kademlia.Routing.null
45 , Network.Kademlia.Routing.full
46 , thisId
47 , shape
48 , Network.Kademlia.Routing.size
49 , Network.Kademlia.Routing.depth
50 , compatibleNodeId
51
52 -- * Lookup
53 , K
54 , defaultK
55 , TableKey (..)
56 , kclosest
57
58 -- * Construction
59 , Network.Kademlia.Routing.nullTable
60 , Event(..)
61 , CheckPing(..)
62 , Network.Kademlia.Routing.insert
63
64 -- * Conversion
65 , Network.Kademlia.Routing.TableEntry
66 , Network.Kademlia.Routing.toList
67
68 -- * Routing
69 , Timestamp
70 , getTimestamp
71 ) -} where
72
73import Control.Applicative as A
74import Control.Arrow
75import Control.Monad
76import Data.Function
77import Data.Functor.Contravariant
78import Data.Functor.Identity
79import Data.List as L hiding (insert)
80import Data.Maybe
81import Data.Monoid
82import Data.Wrapper.PSQ as PSQ
83import Data.Serialize as S hiding (Result, Done)
84import qualified Data.Sequence as Seq
85import Data.Time
86import Data.Time.Clock.POSIX
87import Data.Word
88import GHC.Generics
89import Text.PrettyPrint as PP hiding ((<>))
90import Text.PrettyPrint.HughesPJClass (pPrint,Pretty)
91import qualified Data.ByteString as BS
92import Data.Bits
93import Data.Ord
94import Data.Reflection
95import Network.Address
96import Data.Typeable
97import Data.Coerce
98import Data.Hashable
99
100
101-- | Last time the node was responding to our queries.
102--
103-- Not all nodes that we learn about are equal. Some are \"good\" and
104-- some are not. Many nodes using the DHT are able to send queries
105-- and receive responses, but are not able to respond to queries
106-- from other nodes. It is important that each node's routing table
107-- must contain only known good nodes. A good node is a node has
108-- responded to one of our queries within the last 15 minutes. A
109-- node is also good if it has ever responded to one of our queries
110-- and has sent us a query within the last 15 minutes. After 15
111-- minutes of inactivity, a node becomes questionable. Nodes become
112-- bad when they fail to respond to multiple queries in a row. Nodes
113-- that we know are good are given priority over nodes with unknown
114-- status.
115--
116type Timestamp = POSIXTime
117
118getTimestamp :: IO Timestamp
119getTimestamp = do
120 utcTime <- getCurrentTime
121 return $ utcTimeToPOSIXSeconds utcTime
122
123
124
125{-----------------------------------------------------------------------
126 Bucket
127-----------------------------------------------------------------------}
128--
129-- When a k-bucket is full and a new node is discovered for that
130-- k-bucket, the least recently seen node in the k-bucket is
131-- PINGed. If the node is found to be still alive, the new node is
132-- place in a secondary list, a replacement cache. The replacement
133-- cache is used only if a node in the k-bucket stops responding. In
134-- other words: new nodes are used only when older nodes disappear.
135
136-- | Timestamp - last time this node is pinged.
137type NodeEntry ni = Binding ni Timestamp
138
139
140-- | Maximum number of 'NodeInfo's stored in a bucket. Most clients
141-- use this value.
142defaultBucketSize :: Int
143defaultBucketSize = 8
144
145data QueueMethods m elem fifo = QueueMethods
146 { pushBack :: elem -> fifo -> m fifo
147 , popFront :: fifo -> m (Maybe elem, fifo)
148 , emptyQueue :: m fifo
149 }
150
151{-
152fromQ :: Functor m =>
153 ( a -> b )
154 -> ( b -> a )
155 -> QueueMethods m elem a
156 -> QueueMethods m elem b
157fromQ embed project QueueMethods{..} =
158 QueueMethods { pushBack = \e -> fmap embed . pushBack e . project
159 , popFront = fmap (second embed) . popFront . project
160 , emptyQueue = fmap embed emptyQueue
161 }
162-}
163
164seqQ :: QueueMethods Identity ni (Seq.Seq ni)
165seqQ = QueueMethods
166 { pushBack = \e fifo -> pure (fifo Seq.|> e)
167 , popFront = \fifo -> case Seq.viewl fifo of
168 e Seq.:< fifo' -> pure (Just e, fifo')
169 Seq.EmptyL -> pure (Nothing, Seq.empty)
170 , emptyQueue = pure Seq.empty
171 }
172
173type BucketQueue ni = Seq.Seq ni
174
175bucketQ :: QueueMethods Identity ni (BucketQueue ni)
176bucketQ = seqQ
177
178
179data Compare a = Compare (a -> a -> Ordering) (Int -> a -> Int)
180
181contramapC :: (b -> a) -> Compare a -> Compare b
182contramapC f (Compare cmp hsh) = Compare (\a b -> cmp (f a) (f b))
183 (\s x -> hsh s (f x))
184
185newtype Ordered' s a = Ordered a
186 deriving (Show)
187
188-- | Hack to avoid UndecidableInstances
189newtype Shrink a = Shrink a
190 deriving (Show)
191
192type Ordered s a = Ordered' s (Shrink a)
193
194instance Reifies s (Compare a) => Eq (Ordered' s (Shrink a)) where
195 a == b = (compare a b == EQ)
196
197instance Reifies s (Compare a) => Ord (Ordered' s (Shrink a)) where
198 compare a b = cmp (coerce a) (coerce b)
199 where Compare cmp _ = reflect (Proxy :: Proxy s)
200
201instance Reifies s (Compare a) => Hashable (Ordered' s (Shrink a)) where
202 hashWithSalt salt x = hash salt (coerce x)
203 where Compare _ hash = reflect (Proxy :: Proxy s)
204
205-- | Bucket is also limited in its length — thus it's called k-bucket.
206-- When bucket becomes full, we should split it in two lists by
207-- current span bit. Span bit is defined by depth in the routing
208-- table tree. Size of the bucket should be choosen such that it's
209-- very unlikely that all nodes in bucket fail within an hour of
210-- each other.
211data Bucket s ni = Bucket
212 { bktNodes :: !(PSQ (Ordered s ni) Timestamp) -- current routing nodes
213 , bktQ :: !(BucketQueue (Timestamp,ni)) -- replacements pending time-outs
214 } deriving (Generic)
215
216#define CAN_SHOW_BUCKET 0
217
218#if CAN_SHOW_BUCKET
219deriving instance Show ni => Show (Bucket s ni)
220#endif
221
222bucketCompare :: forall p ni s. Reifies s (Compare ni) => p (Bucket s ni) -> Compare ni
223bucketCompare _ = reflect (Proxy :: Proxy s)
224
225mapBucket :: ( Reifies s (Compare a)
226 , Reifies t (Compare ni)
227 ) => (a -> ni) -> Bucket s a -> Bucket t ni
228mapBucket f (Bucket ns q) = Bucket (PSQ.fromList $ map (\(ni :-> tm) -> (f' ni :-> tm)) $ PSQ.toList ns)
229 (fmap (second f) q)
230 where f' = coerce . f . coerce
231
232
233#if 0
234
235{-
236getGenericNode :: ( Serialize (NodeId)
237 , Serialize ip
238 , Serialize u
239 ) => Get (NodeInfo)
240getGenericNode = do
241 nid <- get
242 naddr <- get
243 u <- get
244 return NodeInfo
245 { nodeId = nid
246 , nodeAddr = naddr
247 , nodeAnnotation = u
248 }
249
250putGenericNode :: ( Serialize (NodeId)
251 , Serialize ip
252 , Serialize u
253 ) => NodeInfo -> Put
254putGenericNode (NodeInfo nid naddr u) = do
255 put nid
256 put naddr
257 put u
258
259instance (Eq ip, Ord (NodeId), Serialize (NodeId), Serialize ip, Serialize u) => Serialize (Bucket) where
260 get = Bucket . psqFromPairList <$> getListOf ( (,) <$> getGenericNode <*> get ) <*> pure (runIdentity $ emptyQueue bucketQ)
261 put = putListOf (\(ni,stamp) -> putGenericNode ni >> put stamp) . psqToPairList . bktNodes
262-}
263
264#endif
265
266psqFromPairList :: (Ord p, PSQKey k) => [(k, p)] -> PSQ k p
267psqFromPairList xs = PSQ.fromList $ map (\(a,b) -> a :-> b) xs
268
269psqToPairList :: ( PSQKey t, Ord t1 ) => PSQ t t1 -> [(t, t1)]
270psqToPairList psq = map (\(a :-> b) -> (a,b)) $ PSQ.toList psq
271
272-- | Update interval, in seconds.
273delta :: NominalDiffTime
274delta = 15 * 60
275
276-- | Should maintain a set of stable long running nodes.
277--
278-- Note: pings are triggerd only when a bucket is full.
279updateBucketForInbound :: ( Coercible t1 t
280 , Alternative f
281 , Reifies s (Compare t1)
282 ) => NominalDiffTime -> t1 -> Bucket s t1 -> f ([t], Bucket s t1)
283updateBucketForInbound curTime info bucket
284 -- Just update timestamp if a node is already in bucket.
285 --
286 -- Note PingResult events should only occur for nodes we requested a ping for,
287 -- and those will always already be in the routing queue and will get their
288 -- timestamp updated here, since 'TryInsert' is called on every inbound packet,
289 -- including ping results.
290 | already_have
291 = pure ( [], map_ns $ PSQ.insertWith max (coerce info) curTime )
292 -- bucket is good, but not full => we can insert a new node
293 | PSQ.size (bktNodes bucket) < defaultBucketSize
294 = pure ( [], map_ns $ PSQ.insert (coerce info) curTime )
295 -- If there are any questionable nodes in the bucket have not been
296 -- seen in the last 15 minutes, the least recently seen node is
297 -- pinged. If any nodes in the bucket are known to have become bad,
298 -- then one is replaced by the new node in the next insertBucket
299 -- iteration.
300 | not (L.null stales)
301 = pure ( stales
302 , bucket { -- Update timestamps so that we don't redundantly ping.
303 bktNodes = updateStamps curTime (coerce stales) $ bktNodes bucket
304 -- Update queue with the pending NodeInfo in case of ping fail.
305 , bktQ = runIdentity $ pushBack bucketQ (curTime,info) $ bktQ bucket } )
306 -- When the bucket is full of good nodes, the new node is simply discarded.
307 -- We must return 'A.empty' here to ensure that bucket splitting happens
308 -- inside 'modifyBucket'.
309 | otherwise = A.empty
310 where
311 -- We (take 1) to keep a 1-to-1 correspondence between pending pings and
312 -- waiting nodes in the bktQ. This way, we don't have to worry about what
313 -- to do with failed pings for which there is no ready replacements.
314 stales = -- One stale:
315 do (n :-> t) <- maybeToList $ PSQ.findMin (bktNodes bucket)
316 guard (t < curTime - delta)
317 return $ coerce n
318 -- All stale:
319 -- map key \$ PSQ.atMost (curTime - delta) $ bktNodes bucket
320
321 already_have = maybe False (const True) $ PSQ.lookup (coerce info) (bktNodes bucket)
322
323 map_ns f = bucket { bktNodes = f (bktNodes bucket) }
324 -- map_q f = bucket { bktQ = runIdentity \$ f (bktQ bucket) }
325
326updateBucketForPingResult :: (Applicative f, Reifies s (Compare a)) =>
327 a -> Bool -> Bucket s a -> f ([(a, Maybe (Timestamp, a))], Bucket s a)
328updateBucketForPingResult bad_node got_response bucket
329 = pure ( map (,Nothing) forgotten
330 ++ map (second Just) replacements
331 , Bucket (foldr replace
332 (bktNodes bucket)
333 replacements)
334 popped
335 )
336 where
337 (top, popped) = runIdentity $ popFront bucketQ (bktQ bucket)
338
339 -- Dropped from accepted, replaced by pending.
340 replacements | got_response = [] -- Timestamp was already updated by TryInsert.
341 | Just info <- top = do
342 -- Insert only if there's a removal.
343 _ <- maybeToList $ PSQ.lookup (coerce bad_node) (bktNodes bucket)
344 return (bad_node, info)
345 | otherwise = []
346
347 -- Dropped from the pending queue without replacing.
348 forgotten | got_response = maybeToList $ fmap snd top
349 | otherwise = []
350
351
352 replace (bad_node, (tm, info)) =
353 PSQ.insert (coerce info) tm
354 . PSQ.delete (coerce bad_node)
355
356
357updateStamps :: PSQKey ni => Timestamp -> [ni] -> PSQ ni Timestamp -> PSQ ni Timestamp
358updateStamps curTime stales nodes = foldl' (\q n -> PSQ.insert n curTime q) nodes stales
359
360type BitIx = Word
361
362partitionQ :: Monad f => QueueMethods f elem b -> (elem -> Bool) -> b -> f (b, b)
363partitionQ imp test q0 = do
364 pass0 <- emptyQueue imp
365 fail0 <- emptyQueue imp
366 let flipfix a b f = fix f a b
367 flipfix q0 (pass0,fail0) $ \rec q qs -> do
368 (mb,q') <- popFront imp q
369 case mb of
370 Nothing -> return qs
371 Just e -> do qs' <- select (pushBack imp e) qs
372 rec q' qs'
373 where
374 select :: Functor f => (b -> f b) -> (b, b) -> f (b, b)
375 select f = if test e then \(a,b) -> flip (,) b <$> f a
376 else \(a,b) -> (,) a <$> f b
377
378
379
380split :: -- ( Eq ip , Ord (NodeId) , FiniteBits (NodeId)) =>
381 forall ni s. ( Reifies s (Compare ni) ) =>
382 (ni -> Word -> Bool)
383 -> BitIx -> Bucket s ni -> (Bucket s ni, Bucket s ni)
384split testNodeIdBit i b = (Bucket ns qs, Bucket ms rs)
385 where
386 (ns,ms) = (PSQ.fromList *** PSQ.fromList) . partition (spanBit . coerce . key) . PSQ.toList $ bktNodes b
387 (qs,rs) = runIdentity $ partitionQ bucketQ (spanBit . snd) $ bktQ b
388
389 spanBit :: ni -> Bool
390 spanBit entry = testNodeIdBit entry i
391
392
393{-----------------------------------------------------------------------
394-- BucketList
395-----------------------------------------------------------------------}
396
397defaultBucketCount :: Int
398defaultBucketCount = 20
399
400defaultMaxBucketCount :: Word
401defaultMaxBucketCount = 24
402
403data Info ni nid = Info
404 { myBuckets :: BucketList ni
405 , myNodeId :: nid
406 , myAddress :: SockAddr
407 }
408 deriving Generic
409
410deriving instance (Eq ni, Eq nid) => Eq (Info ni nid)
411deriving instance (Show ni, Show nid) => Show (Info ni nid)
412
413-- instance (Eq ip, Serialize ip) => Serialize (Info ip)
414
415-- | The routing table covers the entire 'NodeId' space from 0 to 2 ^
416-- 160. The routing table is subdivided into 'Bucket's that each cover
417-- a portion of the space. An empty table has one bucket with an ID
418-- space range of @min = 0, max = 2 ^ 160@. When a node with ID \"N\"
419-- is inserted into the table, it is placed within the bucket that has
420-- @min <= N < max@. An empty table has only one bucket so any node
421-- must fit within it. Each bucket can only hold 'K' nodes, currently
422-- eight, before becoming 'Full'. When a bucket is full of known good
423-- nodes, no more nodes may be added unless our own 'NodeId' falls
424-- within the range of the 'Bucket'. In that case, the bucket is
425-- replaced by two new buckets each with half the range of the old
426-- bucket and the nodes from the old bucket are distributed among the
427-- two new ones. For a new table with only one bucket, the full bucket
428-- is always split into two new buckets covering the ranges @0..2 ^
429-- 159@ and @2 ^ 159..2 ^ 160@.
430--
431data BucketList ni = forall s. Reifies s (Compare ni) =>
432 BucketList { thisNode :: !ni
433 -- | Non-empty list of buckets.
434 , buckets :: [Bucket s ni]
435 }
436
437mapTable :: (b -> t) -> (t -> b) -> BucketList t -> BucketList b
438mapTable g f tbl@(BucketList self bkts) = reify (contramapC g $ bucketCompare bkts)
439 $ \p -> BucketList
440 { thisNode = f self
441 , buckets = map (resolve p . mapBucket f) bkts
442 }
443 where
444 resolve :: Proxy s -> Bucket s ni -> Bucket s ni
445 resolve = const id
446
447instance (Eq ni) => Eq (BucketList ni) where
448 (==) = (==) `on` Network.Kademlia.Routing.toList
449
450#if 0
451
452instance Serialize NominalDiffTime where
453 put = putWord32be . fromIntegral . fromEnum
454 get = (toEnum . fromIntegral) <$> getWord32be
455
456#endif
457
458#if CAN_SHOW_BUCKET
459deriving instance (Show ni) => Show (BucketList ni)
460#else
461instance Show ni => Show (BucketList ni) where
462 showsPrec d (BucketList self bkts) =
463 mappend "BucketList "
464 . showsPrec (d+1) self
465 . mappend " (fromList "
466 . showsPrec (d+1) (L.map (L.map tableEntry . PSQ.toList . bktNodes) $ bkts)
467 . mappend ") "
468#endif
469
470#if 0
471
472-- | Normally, routing table should be saved between invocations of
473-- the client software. Note that you don't need to store /this/
474-- 'NodeId' since it is already included in routing table.
475instance (Eq ip, Serialize ip, Ord (NodeId), Serialize (NodeId), Serialize u) => Serialize (BucketList)
476
477#endif
478
479-- | Shape of the table.
480instance Pretty (BucketList ni) where
481 pPrint t
482 | bucketCount < 6 = hcat $ punctuate ", " $ L.map PP.int ss
483 | otherwise = brackets $
484 PP.int (L.sum ss) <> " nodes, " <>
485 PP.int bucketCount <> " buckets"
486 where
487 bucketCount = L.length ss
488 ss = shape t
489
490-- | Empty table with specified /spine/ node id.
491--
492-- XXX: The comparison function argument is awkward here.
493nullTable :: (ni -> ni -> Ordering) -> (Int -> ni -> Int) -> ni -> Int -> BucketList ni
494nullTable cmp hsh ni n =
495 reify (Compare cmp hsh)
496 $ \p -> BucketList
497 ni
498 [Bucket (empty p) (runIdentity $ emptyQueue bucketQ)]
499 where
500 empty :: Reifies s (Compare ni) => Proxy s -> PSQ (Ordered s ni) Timestamp
501 empty = const $ PSQ.empty
502
503#if 0
504
505-- | Test if table is empty. In this case DHT should start
506-- bootstrapping process until table becomes 'full'.
507null :: BucketList -> Bool
508null (Tip _ _ b) = PSQ.null $ bktNodes b
509null _ = False
510
511-- | Test if table have maximum number of nodes. No more nodes can be
512-- 'insert'ed, except old ones becomes bad.
513full :: BucketList -> Bool
514full (Tip _ n _) = n == 0
515full (Zero t b) = PSQ.size (bktNodes b) == defaultBucketSize && full t
516full (One b t) = PSQ.size (bktNodes b) == defaultBucketSize && full t
517
518-- | Get the /spine/ node id.
519thisId :: BucketList -> NodeId
520thisId (Tip nid _ _) = nid
521thisId (Zero table _) = thisId table
522thisId (One _ table) = thisId table
523
524-- | Number of nodes in a bucket or a table.
525type NodeCount = Int
526
527#endif
528
529-- | Internally, routing table is similar to list of buckets or a
530-- /matrix/ of nodes. This function returns the shape of the matrix.
531shape :: BucketList ni -> [Int]
532shape (BucketList _ tbl) = map (PSQ.size . bktNodes) tbl
533
534#if 0
535
536-- | Get number of nodes in the table.
537size :: BucketList -> NodeCount
538size = L.sum . shape
539
540-- | Get number of buckets in the table.
541depth :: BucketList -> BucketCount
542depth = L.length . shape
543
544#endif
545
546lookupBucket :: forall ni nid x.
547 ( -- FiniteBits nid
548 Ord nid
549 ) => KademliaSpace nid ni -> nid -> (forall s. Reifies s (Compare ni) => [Bucket s ni] -> x) -> BucketList ni -> x
550lookupBucket space nid kont (BucketList self bkts) = kont $ go 0 [] bkts
551 where
552 d = kademliaXor space nid (kademliaLocation space self)
553
554 go :: Word -> [Bucket s ni] -> [Bucket s ni] -> [Bucket s ni]
555 go i bs (bucket : buckets)
556 | kademliaTestBit space d i = bucket : buckets ++ bs
557 | otherwise = go (succ i) (bucket:bs) buckets
558 go _ bs [] = bs
559
560bucketNumber :: forall ni nid.
561 KademliaSpace nid ni -> nid -> BucketList ni -> Int
562bucketNumber space nid (BucketList self bkts) = fromIntegral $ go 0 bkts
563 where
564 d = kademliaXor space nid (kademliaLocation space self)
565
566 go :: Word -> [Bucket s ni] -> Word
567 go i (bucket : buckets)
568 | kademliaTestBit space d i = i
569 | otherwise = go (succ i) buckets
570 go i [] = i
571
572
573compatibleNodeId :: forall ni nid.
574 ( Serialize nid, FiniteBits nid) =>
575 (ni -> nid) -> BucketList ni -> IO nid
576compatibleNodeId nodeId tbl = genBucketSample prefix br
577 where
578 br = bucketRange (L.length (shape tbl) - 1) True
579 nodeIdSize = finiteBitSize (undefined :: nid) `div` 8
580 bs = BS.pack $ take nodeIdSize $ tablePrefix (testIdBit . nodeId) tbl ++ repeat 0
581 prefix = either error id $ S.decode bs
582
583tablePrefix :: (ni -> Word -> Bool) -> BucketList ni -> [Word8]
584tablePrefix testbit = map (packByte . take 8 . (++repeat False))
585 . chunksOf 8
586 . tableBits testbit
587 where
588 packByte = foldl1' (.|.) . zipWith bitmask [7,6 .. 0]
589 bitmask ix True = bit ix
590 bitmask _ _ = 0
591
592tableBits :: (ni -> Word -> Bool) -> BucketList ni -> [Bool]
593tableBits testbit (BucketList self bkts) =
594 zipWith const (map (testbit self) [0..])
595 bkts
596
597selfNode :: BucketList ni -> ni
598selfNode (BucketList self _) = self
599
600chunksOf :: Int -> [e] -> [[e]]
601chunksOf i ls = map (take i) (build (splitter ls)) where
602 splitter :: [e] -> ([e] -> a -> a) -> a -> a
603 splitter [] _ n = n
604 splitter l c n = l `c` splitter (drop i l) c n
605
606build :: ((a -> [a] -> [a]) -> [a] -> [a]) -> [a]
607build g = g (:) []
608
609
610
611-- | Count of closest nodes in find_node reply.
612type K = Int
613
614-- | Default 'K' is equal to 'defaultBucketSize'.
615defaultK :: K
616defaultK = 8
617
618#if 0
619class TableKey dht k where
620 toNodeId :: k -> NodeId
621
622instance TableKey dht (NodeId) where
623 toNodeId = id
624
625#endif
626
627-- | In Kademlia, the distance metric is XOR and the result is
628-- interpreted as an unsigned integer.
629newtype NodeDistance nodeid = NodeDistance nodeid
630 deriving (Eq, Ord)
631
632-- | distance(A,B) = |A xor B| Smaller values are closer.
633distance :: Bits nid => nid -> nid -> NodeDistance nid
634distance a b = NodeDistance $ xor a b
635
636-- | Order by closeness: nearest nodes first.
637rank :: ( Ord nid
638 ) => KademliaSpace nid ni -> nid -> [ni] -> [ni]
639rank space nid = L.sortBy (comparing (kademliaXor space nid . kademliaLocation space))
640
641
642-- | Get a list of /K/ closest nodes using XOR metric. Used in
643-- 'find_node' and 'get_peers' queries.
644kclosest :: ( -- FiniteBits nid
645 Ord nid
646 ) =>
647 KademliaSpace nid ni -> Int -> nid -> BucketList ni -> [ni]
648kclosest space k nid tbl = take k $ rank space nid (L.concat bucket)
649 ++ rank space nid (L.concat everyone)
650 where
651 (bucket,everyone) =
652 L.splitAt 1
653 . lookupBucket space nid (L.map (coerce . L.map PSQ.key . PSQ.toList . bktNodes))
654 $ tbl
655
656
657
658{-----------------------------------------------------------------------
659-- Routing
660-----------------------------------------------------------------------}
661
662splitTip :: -- ( Eq ip , Ord (NodeId) , FiniteBits (NodeId)) =>
663 ( Reifies s (Compare ni) ) =>
664 (ni -> Word -> Bool)
665 -> ni -> BitIx -> Bucket s ni -> [ Bucket s ni ]
666splitTip testNodeBit ni i bucket
667 | testNodeBit ni i = [zeros , ones ]
668 | otherwise = [ones , zeros ]
669 where
670 (ones, zeros) = split testNodeBit i bucket
671
672-- | Used in each query.
673--
674-- TODO: Kademlia non-empty subtrees should should split if they have less than
675-- k nodes in them. Which subtrees I mean is illustrated in Fig 1. of Kademlia
676-- paper. The rule requiring additional splits is in section 2.4.
677modifyBucket
678 :: -- ( Eq ip , Ord (NodeId) , FiniteBits (NodeId)) =>
679 forall ni nid xs.
680 KademliaSpace nid ni
681 -> nid -> (forall s. Reifies s (Compare ni) => Bucket s ni -> Maybe (xs, Bucket s ni)) -> BucketList ni -> Maybe (xs,BucketList ni)
682modifyBucket space nid f (BucketList self bkts)
683 = second (BucketList self) <$> go (0 :: BitIx) bkts
684 where
685 d = kademliaXor space nid (kademliaLocation space self)
686
687 -- go :: BitIx -> [Bucket s ni] -> Maybe (xs, [Bucket s ni])
688
689 go !i (bucket : buckets@(_:_))
690 | kademliaTestBit space d i = second (: buckets) <$> f bucket
691 | otherwise = second (bucket :) <$> go (succ i) buckets
692
693 go !i [bucket] = second (: []) <$> f bucket <|> gosplit
694 where
695 gosplit | i < defaultMaxBucketCount = go i (splitTip ( kademliaTestBit space
696 . kademliaLocation space )
697 self
698 i
699 bucket)
700 | otherwise = Nothing -- Limit the number of buckets.
701
702
703bktCount :: BucketList ni -> Int
704bktCount (BucketList _ bkts) = L.length bkts
705
706-- | Triggering event for atomic table update
707data Event ni = TryInsert { foreignNode :: ni }
708 | PingResult { foreignNode :: ni , ponged :: Bool }
709
710#if 0
711deriving instance Eq (NodeId) => Eq (Event)
712deriving instance ( Show ip
713 , Show (NodeId)
714 , Show u
715 ) => Show (Event)
716
717#endif
718
719eventId :: (ni -> nid) -> Event ni -> nid
720eventId nodeId (TryInsert ni) = nodeId ni
721eventId nodeId (PingResult ni _) = nodeId ni
722
723
724-- | Actions requested by atomic table update
725data CheckPing ni = CheckPing [ni]
726
727#if 0
728
729deriving instance Eq (NodeId) => Eq (CheckPing)
730deriving instance ( Show ip
731 , Show (NodeId)
732 , Show u
733 ) => Show (CheckPing)
734
735#endif
736
737
738-- | Call on every inbound packet (including requested ping results).
739-- Returns a triple (was_inserted, to_ping, tbl') where
740--
741-- [ /was_inserted/ ] True if the node was added to the routing table.
742--
743-- [ /to_ping/ ] A list of nodes to ping and then run 'updateForPingResult'.
744-- This will be empty if /was_inserted/, but a non-inserted node
745-- may be added to a replacement queue and will be inserted if
746-- one of the items in this list time out.
747--
748-- [ /tbl'/ ] The updated routing 'BucketList'.
749--
750updateForInbound ::
751 KademliaSpace nid ni
752 -> Timestamp -> ni -> BucketList ni -> (Bool, [ni], BucketList ni)
753updateForInbound space tm ni tbl@(BucketList _ bkts) =
754 maybe (False, [],tbl) (\(ps,tbl') -> (True, ps, tbl'))
755 $ modifyBucket space
756 (kademliaLocation space ni)
757 (updateBucketForInbound tm ni)
758 tbl
759
760-- | Update the routing table with the results of a ping.
761--
762-- Each (a,(tm,b)) in the returned list indicates that the node /a/ was deleted from the
763-- routing table and the node /b/, with timestamp /tm/, has taken its place.
764updateForPingResult ::
765 KademliaSpace nid ni
766 -> ni -- ^ The pinged node.
767 -> Bool -- ^ True if we got a reply, False if it timed out.
768 -> BucketList ni -- ^ The routing table.
769 -> ( [(ni,Maybe (Timestamp, ni))], BucketList ni )
770updateForPingResult space ni got_reply tbl =
771 fromMaybe ([],tbl)
772 $ modifyBucket space
773 (kademliaLocation space ni)
774 (updateBucketForPingResult ni got_reply)
775 tbl
776
777
778{-----------------------------------------------------------------------
779-- Conversion
780-----------------------------------------------------------------------}
781
782type TableEntry ni = (ni, Timestamp)
783
784tableEntry :: NodeEntry ni -> TableEntry ni
785tableEntry (a :-> b) = (a, b)
786
787toList :: BucketList ni -> [[TableEntry ni]]
788toList (BucketList _ bkts) = coerce $ L.map (L.map tableEntry . PSQ.toList . bktNodes) bkts
789
790data KademliaSpace nid ni = KademliaSpace
791 { -- | Given a node record (probably including IP address), yields a
792 -- kademlia xor-metric location.
793 kademliaLocation :: ni -> nid
794 -- | Used when comparing locations. This is similar to
795 -- 'Data.Bits.testBit' except that the ordering of bits is reversed, so
796 -- that 0 is the most significant bit.
797 , kademliaTestBit :: nid -> Word -> Bool
798 -- | The Kademlia xor-metric.
799 , kademliaXor :: nid -> nid -> nid
800
801 , kademliaSample :: forall m. Applicative m => (Int -> m BS.ByteString) -> nid -> (Int,Word8,Word8) -> m nid
802 }
803
804instance Contravariant (KademliaSpace nid) where
805 contramap f ks = ks
806 { kademliaLocation = kademliaLocation ks . f
807 }
808
diff --git a/src/Network/Kademlia/Search.hs b/src/Network/Kademlia/Search.hs
new file mode 100644
index 00000000..593470c3
--- /dev/null
+++ b/src/Network/Kademlia/Search.hs
@@ -0,0 +1,247 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE PatternSynonyms #-}
3{-# LANGUAGE RecordWildCards #-}
4{-# LANGUAGE ScopedTypeVariables #-}
5{-# LANGUAGE FlexibleContexts #-}
6{-# LANGUAGE LambdaCase #-}
7module Network.Kademlia.Search where
8
9import Control.Concurrent.Tasks
10import Control.Concurrent.STM
11import Control.Exception
12import Control.Monad
13import Data.Bool
14import Data.Function
15import Data.List
16import qualified Data.Map.Strict as Map
17 ;import Data.Map.Strict (Map)
18import Data.Maybe
19import qualified Data.Set as Set
20 ;import Data.Set (Set)
21import Data.Hashable (Hashable(..)) -- for type sigs
22import System.IO
23import System.IO.Error
24
25import qualified Data.MinMaxPSQ as MM
26 ;import Data.MinMaxPSQ (MinMaxPSQ, MinMaxPSQ')
27import qualified Data.Wrapper.PSQ as PSQ
28 ;import Data.Wrapper.PSQ (pattern (:->), Binding, pattern Binding, Binding', PSQ, PSQKey)
29import Network.Address hiding (NodeId)
30import Network.Kademlia.Routing as R
31#ifdef THREAD_DEBUG
32import Control.Concurrent.Lifted.Instrument
33#else
34import Control.Concurrent.Lifted
35import GHC.Conc (labelThread)
36#endif
37
38data Search nid addr tok ni r = Search
39 { searchSpace :: KademliaSpace nid ni
40 , searchNodeAddress :: ni -> addr
41 , searchQuery :: nid -> ni -> IO (Maybe ([ni], [r], Maybe tok))
42 }
43
44data SearchState nid addr tok ni r = SearchState
45 {-
46 { searchParams :: Search nid addr ni r
47
48 , searchTarget :: nid
49 -- | This action will be performed at least once on each search result.
50 -- It may be invoked multiple times since different nodes may report the
51 -- same result. If the action returns 'False', the search will be
52 -- aborted, otherwise it will continue until it is decided that we've
53 -- asked the closest K nodes to the target.
54 , searchResult :: r -> STM Bool
55
56 -}
57
58 { -- | The number of pending queries. Incremented before any query is sent
59 -- and decremented when we get a reply.
60 searchPendingCount :: TVar Int
61 -- | Nodes scheduled to be queried.
62 , searchQueued :: TVar (MinMaxPSQ ni nid)
63 -- | The nearest (K - α) nodes that issued a reply.
64 , searchInformant :: TVar (MinMaxPSQ' ni nid (Maybe tok))
65 -- | This tracks already-queried addresses so we avoid bothering them
66 -- again. XXX: We could probably keep only the pending queries in this
67 -- set. It also can be a bounded 'MinMaxPSQ', although searchAlpha
68 -- should limit the number of outstanding queries.
69 , searchVisited :: TVar (Set addr)
70 }
71
72newSearch :: ( Ord addr
73 , PSQKey nid
74 , PSQKey ni
75 ) =>
76 {-
77 KademliaSpace nid ni
78 -> (ni -> addr)
79 -> (ni -> IO ([ni], [r])) -- the query action.
80 -> (r -> STM Bool) -- receives search results.
81 -> nid -- target of search
82 -}
83 Search nid addr tok ni r
84 -> nid
85 -> [ni] -- Initial nodes to query.
86 -> STM (SearchState nid addr tok ni r)
87newSearch (Search space nAddr qry) target ns = do
88 c <- newTVar 0
89 q <- newTVar $ MM.fromList
90 $ map (\n -> n :-> kademliaXor space target (kademliaLocation space n))
91 $ ns
92 i <- newTVar MM.empty
93 v <- newTVar Set.empty
94 return -- (Search space nAddr qry) , r , target
95 ( SearchState c q i v )
96
97-- | Discard a value from a key-priority-value tuple. This is useful for
98-- swaping items from a "MinMaxPSQ'" to a "MinMaxPSQ".
99stripValue :: Binding' k p v -> Binding k p
100stripValue (Binding ni _ nid) = (ni :-> nid)
101
102-- | Reset a 'SearchState' object to ready it for a repeated search.
103reset :: (Ord ni, Ord nid, Hashable ni, Hashable nid) =>
104 TVar (BucketList ni)
105 -> Search nid addr1 tok1 ni r1
106 -> nid
107 -> SearchState nid addr tok ni r
108 -> STM (SearchState nid addr tok ni r)
109reset bkts qsearch target st = do
110 searchIsFinished st >>= check -- Wait for a search to finish before resetting.
111 bktNodes <- map (\ni -> ni :-> kademliaLocation (searchSpace qsearch) ni)
112 . R.kclosest (searchSpace qsearch) searchK target
113 <$> readTVar bkts
114 priorInformants <- map stripValue . MM.toList <$> readTVar (searchInformant st)
115 writeTVar (searchQueued st) $ MM.fromList $ priorInformants ++ bktNodes
116 writeTVar (searchInformant st) MM.empty
117 writeTVar (searchVisited st) Set.empty
118 writeTVar (searchPendingCount st) 0
119 return st
120
121searchAlpha :: Int
122searchAlpha = 8
123
124-- | 'searchK' should be larger than 'searchAlpha'. How much larger depends on
125-- how fast the queries are. For Tox's much slower onion-routed queries, we
126-- need to ensure that closer non-responding queries don't completely push out
127-- farther away queries.
128--
129-- For BitTorrent, setting them both 8 was not an issue, but that is no longer
130-- supported because now the number of remembered informants is now the
131-- difference between these two numbers. So, if searchK = 16 and searchAlpha =
132-- 4, then the number of remembered query responses is 12.
133searchK :: Int
134searchK = 16
135
136sendQuery :: forall addr nid tok ni r.
137 ( Ord addr
138 , PSQKey nid
139 , PSQKey ni
140 , Show nid
141 ) =>
142 Search nid addr tok ni r
143 -> nid
144 -> (r -> STM Bool) -- ^ return False to terminate the search.
145 -> SearchState nid addr tok ni r
146 -> Binding ni nid
147 -> IO ()
148sendQuery Search{..} searchTarget searchResult sch@SearchState{..} (ni :-> d) = do
149 myThreadId >>= flip labelThread ("searchQuery." ++ show searchTarget)
150 reply <- searchQuery searchTarget ni `catchIOError` const (return Nothing)
151 -- (ns,rs)
152 let tok = error "TODO: token"
153 atomically $ do
154 modifyTVar searchPendingCount pred
155 maybe (return ()) go reply
156 where
157 go (ns,rs,tok) = do
158 vs <- readTVar searchVisited
159 -- We only queue a node if it is not yet visited
160 let insertFoundNode :: Int
161 -> ni
162 -> MinMaxPSQ ni nid
163 -> MinMaxPSQ ni nid
164 insertFoundNode k n q
165 | searchNodeAddress n `Set.member` vs
166 = q
167 | otherwise = MM.insertTake k n ( kademliaXor searchSpace searchTarget
168 $ kademliaLocation searchSpace n )
169 q
170 qsize0 <- MM.size <$> readTVar searchQueued
171 let qsize = if qsize0 < searchK then searchK else qsize0
172 modifyTVar searchQueued $ \q -> foldr (insertFoundNode qsize) q ns
173 modifyTVar searchInformant $ MM.insertTake' (searchK - searchAlpha) ni tok d
174 flip fix rs $ \loop -> \case
175 r:rs' -> do
176 wanting <- searchResult r
177 if wanting then loop rs'
178 else searchCancel sch
179 [] -> return ()
180
181
182searchIsFinished :: ( PSQKey nid
183 , PSQKey ni
184 ) => SearchState nid addr tok ni r -> STM Bool
185searchIsFinished SearchState{ ..} = do
186 q <- readTVar searchQueued
187 cnt <- readTVar searchPendingCount
188 informants <- readTVar searchInformant
189 return $ cnt == 0
190 && ( MM.null q
191 || ( MM.size informants >= (searchK - searchAlpha)
192 && ( PSQ.prio (fromJust $ MM.findMax informants)
193 <= PSQ.prio (fromJust $ MM.findMin q))))
194
195searchCancel :: SearchState nid addr tok ni r -> STM ()
196searchCancel SearchState{..} = do
197 writeTVar searchPendingCount 0
198 writeTVar searchQueued MM.empty
199
200search ::
201 ( Ord r
202 , Ord addr
203 , PSQKey nid
204 , PSQKey ni
205 , Show nid
206 ) => Search nid addr tok ni r -> R.BucketList ni -> nid -> (r -> STM Bool) -> IO (SearchState nid addr tok ni r)
207search sch buckets target result = do
208 let ns = R.kclosest (searchSpace sch) searchK target buckets
209 st <- atomically $ newSearch sch target ns
210 fork $ searchLoop sch target result st
211 return st
212
213searchLoop :: ( Ord addr, Ord nid, Ord ni, Show nid, Hashable nid, Hashable ni )
214 => Search nid addr tok ni r -- ^ Query and distance methods.
215 -> nid -- ^ The target we are searching for.
216 -> (r -> STM Bool) -- ^ Invoked on each result. Return False to quit searching.
217 -> SearchState nid addr tok ni r -- ^ Search-related state.
218 -> IO ()
219searchLoop sch@Search{..} target result s@SearchState{..} = do
220 myThreadId >>= flip labelThread ("search."++show target)
221 withTaskGroup ("search.g."++show target) searchAlpha $ \g -> fix $ \again -> do
222 join $ atomically $ do
223 cnt <- readTVar $ searchPendingCount
224 informants <- readTVar searchInformant
225 found <- MM.minView <$> readTVar searchQueued
226 case found of
227 Just (ni :-> d, q)
228 | -- If there's fewer than /k/ informants and there's any
229 -- node we haven't yet got a response from.
230 (MM.size informants < searchK - searchAlpha) && (cnt > 0 || not (MM.null q))
231 -- Or there's no informants yet at all.
232 || MM.null informants
233 -- Or if the closest scheduled node is nearer than the
234 -- nearest /k/ informants.
235 || (d < PSQ.prio (fromJust $ MM.findMax informants))
236 -> -- Then the search continues, send a query.
237 do writeTVar searchQueued q
238 modifyTVar searchVisited $ Set.insert (searchNodeAddress ni)
239 modifyTVar searchPendingCount succ
240 return $ do
241 forkTask g
242 "searchQuery"
243 $ sendQuery sch target result s (ni :-> d)
244 again
245 _ -> -- Otherwise, we are finished.
246 do check (cnt == 0)
247 return $ return ()
diff --git a/src/Network/QueryResponse.hs b/src/Network/QueryResponse.hs
new file mode 100644
index 00000000..0345dd88
--- /dev/null
+++ b/src/Network/QueryResponse.hs
@@ -0,0 +1,549 @@
1-- | This module can implement any query\/response protocol. It was written
2-- with Kademlia implementations in mind.
3
4{-# LANGUAGE CPP #-}
5{-# LANGUAGE GADTs #-}
6{-# LANGUAGE LambdaCase #-}
7{-# LANGUAGE PartialTypeSignatures #-}
8{-# LANGUAGE RankNTypes #-}
9{-# LANGUAGE ScopedTypeVariables #-}
10{-# LANGUAGE TupleSections #-}
11module Network.QueryResponse where
12
13#ifdef THREAD_DEBUG
14import Control.Concurrent.Lifted.Instrument
15#else
16import Control.Concurrent
17import GHC.Conc (labelThread)
18#endif
19import Control.Concurrent.STM
20import Control.Exception
21import Control.Monad
22import qualified Data.ByteString as B
23 ;import Data.ByteString (ByteString)
24import Data.Function
25import Data.Functor.Contravariant
26import qualified Data.IntMap.Strict as IntMap
27 ;import Data.IntMap.Strict (IntMap)
28import qualified Data.Map.Strict as Map
29 ;import Data.Map.Strict (Map)
30import qualified Data.Word64Map as W64Map
31 ;import Data.Word64Map (Word64Map)
32import Data.Word
33import Data.Maybe
34import Data.Typeable
35import Network.Socket
36import Network.Socket.ByteString as B
37import System.Endian
38import System.IO
39import System.IO.Error
40import System.Timeout
41
42-- | Three methods are required to implement a datagram based query\/response protocol.
43data Transport err addr x = Transport
44 { -- | Blocks until an inbound packet is available. Returns 'Nothing' when
45 -- no more packets are expected due to a shutdown or close event.
46 -- Otherwise, the packet will be parsed as type /x/ and an origin address
47 -- /addr/. Parse failure is indicated by the type 'err'.
48 awaitMessage :: forall a. (Maybe (Either err (x, addr)) -> IO a) -> IO a
49 -- | Send an /x/ packet to the given destination /addr/.
50 , sendMessage :: addr -> x -> IO ()
51 -- | Shutdown and clean up any state related to this 'Transport'.
52 , closeTransport :: IO ()
53 }
54
55-- | This function modifies a 'Transport' to use higher-level addresses and
56-- packet representations. It could be used to change UDP 'ByteString's into
57-- bencoded syntax trees or to add an encryption layer in which addresses have
58-- associated public keys.
59layerTransportM ::
60 (x -> addr -> IO (Either err (x', addr')))
61 -- ^ Function that attempts to transform a low-level address/packet
62 -- pair into a higher level representation.
63 -> (x' -> addr' -> IO (x, addr))
64 -- ^ Function to encode a high-level address/packet into a lower level
65 -- representation.
66 -> Transport err addr x
67 -- ^ The low-level transport to be transformed.
68 -> Transport err addr' x'
69layerTransportM parse encode tr =
70 tr { awaitMessage = \kont ->
71 awaitMessage tr $ \m -> mapM (mapM $ uncurry parse) m >>= kont . fmap join
72 , sendMessage = \addr' msg' -> do
73 (msg,addr) <- encode msg' addr'
74 sendMessage tr addr msg
75 }
76
77
78-- | This function modifies a 'Transport' to use higher-level addresses and
79-- packet representations. It could be used to change UDP 'ByteString's into
80-- bencoded syntax trees or to add an encryption layer in which addresses have
81-- associated public keys.
82layerTransport ::
83 (x -> addr -> Either err (x', addr'))
84 -- ^ Function that attempts to transform a low-level address/packet
85 -- pair into a higher level representation.
86 -> (x' -> addr' -> (x, addr))
87 -- ^ Function to encode a high-level address/packet into a lower level
88 -- representation.
89 -> Transport err addr x
90 -- ^ The low-level transport to be transformed.
91 -> Transport err addr' x'
92layerTransport parse encode tr =
93 layerTransportM (\x addr -> return $ parse x addr)
94 (\x' addr' -> return $ encode x' addr')
95 tr
96
97-- | Paritions a 'Transport' into two higher-level transports. Note: An 'MVar'
98-- is used to share the same underlying socket, so be sure to fork a thread for
99-- both returned 'Transport's to avoid hanging.
100partitionTransport :: ((b,a) -> Either (x,xaddr) (b,a))
101 -> ((x,xaddr) -> Maybe (b,a))
102 -> Transport err a b
103 -> IO (Transport err xaddr x, Transport err a b)
104partitionTransport parse encodex tr =
105 partitionTransportM (return . parse) (return . encodex) tr
106
107-- | Paritions a 'Transport' into two higher-level transports. Note: An 'MVar'
108-- is used to share the same underlying socket, so be sure to fork a thread for
109-- both returned 'Transport's to avoid hanging.
110partitionTransportM :: ((b,a) -> IO (Either (x,xaddr) (b,a)))
111 -> ((x,xaddr) -> IO (Maybe (b,a)))
112 -> Transport err a b
113 -> IO (Transport err xaddr x, Transport err a b)
114partitionTransportM parse encodex tr = do
115 mvar <- newEmptyMVar
116 let xtr = tr { awaitMessage = \kont -> fix $ \again -> do
117 awaitMessage tr $ \m -> case m of
118 Just (Right msg) -> parse msg >>=
119 either (kont . Just . Right)
120 (\y -> putMVar mvar y >> again)
121 Just (Left e) -> kont $ Just (Left e)
122 Nothing -> kont Nothing
123 , sendMessage = \addr' msg' -> do
124 msg_addr <- encodex (msg',addr')
125 mapM_ (uncurry . flip $ sendMessage tr) msg_addr
126 }
127 ytr = Transport
128 { awaitMessage = \kont -> takeMVar mvar >>= kont . Just . Right
129 , sendMessage = sendMessage tr
130 , closeTransport = return ()
131 }
132 return (xtr, ytr)
133
134-- |
135-- * f add x --> Nothing, consume x
136-- --> Just id, leave x to a different handler
137-- --> Just g, apply g to x and leave that to a different handler
138addHandler :: (err -> IO ()) -> (addr -> x -> IO (Maybe (x -> x))) -> Transport err addr x -> Transport err addr x
139addHandler onParseError f tr = tr
140 { awaitMessage = \kont -> fix $ \eat -> awaitMessage tr $ \m -> do
141 case m of
142 Just (Right (x, addr)) -> f addr x >>= maybe eat (kont . Just . Right . (, addr) . ($ x))
143 Just (Left e ) -> onParseError e >> kont (Just $ Left e)
144 Nothing -> kont $ Nothing
145 }
146
147-- | Modify a 'Transport' to invoke an action upon every received packet.
148onInbound :: (addr -> x -> IO ()) -> Transport err addr x -> Transport err addr x
149onInbound f tr = addHandler (const $ return ()) (\addr x -> f addr x >> return (Just id)) tr
150
151-- * Using a query\/response client.
152
153-- | Fork a thread that handles inbound packets. The returned action may be used
154-- to terminate the thread and clean up any related state.
155--
156-- Example usage:
157--
158-- > -- Start client.
159-- > quitServer <- forkListener "listener" (clientNet client)
160-- > -- Send a query q, recieve a response r.
161-- > r <- sendQuery client method q
162-- > -- Quit client.
163-- > quitServer
164forkListener :: String -> Transport err addr x -> IO (IO ())
165forkListener name client = do
166 thread_id <- forkIO $ do
167 myThreadId >>= flip labelThread ("listener."++name)
168 fix $ awaitMessage client . const
169 return $ do
170 closeTransport client
171 killThread thread_id
172
173-- | Send a query to a remote peer. Note that this function will always time
174-- out if 'forkListener' was never invoked to spawn a thread to receive and
175-- dispatch the response.
176sendQuery ::
177 forall err a b tbl x meth tid addr.
178 Client err meth tid addr x -- ^ A query/response implementation.
179 -> MethodSerializer tid addr x meth a b -- ^ Information for marshaling the query.
180 -> a -- ^ The outbound query.
181 -> addr -- ^ Destination address of query.
182 -> IO (Maybe b) -- ^ The response, or 'Nothing' if it timed out.
183sendQuery (Client net d err pending whoami _) meth q addr0 = do
184 mvar <- newEmptyMVar
185 (tid,addr,expiry) <- atomically $ do
186 tbl <- readTVar pending
187 (tid, tbl') <- dispatchRegister (tableMethods d) mvar addr0 tbl
188 (addr,expiry) <- methodTimeout meth tid addr0
189 writeTVar pending tbl'
190 return (tid,addr,expiry)
191 self <- whoami (Just addr)
192 mres <- do sendMessage net addr (wrapQuery meth tid self addr q)
193 timeout expiry $ takeMVar mvar
194 `catchIOError` (\e -> return Nothing)
195 case mres of
196 Just x -> return $ Just $ unwrapResponse meth x
197 Nothing -> do
198 atomically $ readTVar pending >>= dispatchCancel (tableMethods d) tid >>= writeTVar pending
199 reportTimeout err (method meth) tid addr
200 return Nothing
201
202-- * Implementing a query\/response 'Client'.
203
204-- | All inputs required to implement a query\/response client.
205data Client err meth tid addr x = forall tbl. Client
206 { -- | The 'Transport' used to dispatch and receive packets.
207 clientNet :: Transport err addr x
208 -- | Methods for handling inbound packets.
209 , clientDispatcher :: DispatchMethods tbl err meth tid addr x
210 -- | Methods for reporting various conditions.
211 , clientErrorReporter :: ErrorReporter addr x meth tid err
212 -- | State necessary for routing inbound responses and assigning unique
213 -- /tid/ values for outgoing queries.
214 , clientPending :: TVar tbl
215 -- | An action yielding this client\'s own address. It is invoked once
216 -- on each outbound and inbound packet. It is valid for this to always
217 -- return the same value.
218 --
219 -- The argument, if supplied, is the remote address for the transaction.
220 -- This can be used to maintain consistent aliases for specific peers.
221 , clientAddress :: Maybe addr -> IO addr
222 -- | Transform a query /tid/ value to an appropriate response /tid/
223 -- value. Normally, this would be the identity transformation, but if
224 -- /tid/ includes a unique cryptographic nonce, then it should be
225 -- generated here.
226 , clientResponseId :: tid -> IO tid
227 }
228
229-- | An incoming message can be classified into three cases.
230data MessageClass err meth tid addr x
231 = IsQuery meth tid -- ^ An unsolicited query is handled based on it's /meth/ value. Any response
232 -- should include the provided /tid/ value.
233 | IsResponse tid -- ^ A response to a outgoing query we associated with a /tid/ value.
234 | IsUnsolicited (addr -> addr -> IO (Maybe (x -> x))) -- ^ Transactionless informative packet. The io action will be invoked
235 -- with the source and destination address of a message. If it handles the
236 -- message, it should return Nothing. Otherwise, it should return a transform
237 -- (usually /id/) to apply before the next handler examines it.
238 | IsUnknown err -- ^ None of the above.
239
240-- | Handler for an inbound query of type /x/ from an address of type _addr_.
241data MethodHandler err tid addr x = forall a b. MethodHandler
242 { -- | Parse the query into a more specific type for this method.
243 methodParse :: x -> Either err a
244 -- | Serialize the response for transmission, given a context /ctx/ and the origin
245 -- and destination addresses.
246 , methodSerialize :: tid -> addr -> addr -> b -> x
247 -- | Fully typed action to perform upon the query. The remote origin
248 -- address of the query is provided to the handler.
249 , methodAction :: addr -> a -> IO b
250 }
251 -- | See also 'IsUnsolicited' which likely makes this constructor unnecessary.
252 | forall a. NoReply
253 { -- | Parse the query into a more specific type for this method.
254 methodParse :: x -> Either err a
255 -- | Fully typed action to perform upon the query. The remote origin
256 -- address of the query is provided to the handler.
257 , noreplyAction :: addr -> a -> IO ()
258 }
259
260contramapAddr :: (a -> b) -> MethodHandler err tid b x -> MethodHandler err tid a x
261contramapAddr f (MethodHandler p s a)
262 = MethodHandler
263 p
264 (\tid src dst result -> s tid (f src) (f dst) result)
265 (\addr arg -> a (f addr) arg)
266contramapAddr f (NoReply p a)
267 = NoReply p (\addr arg -> a (f addr) arg)
268
269
270-- | Attempt to invoke a 'MethodHandler' upon a given inbound query. If the
271-- parse is successful, the returned IO action will construct our reply if
272-- there is one. Otherwise, a parse err is returned.
273dispatchQuery :: MethodHandler err tid addr x -- ^ Handler to invoke.
274 -> tid -- ^ The transaction id for this query\/response session.
275 -> addr -- ^ Our own address, to which the query was sent.
276 -> x -- ^ The query packet.
277 -> addr -- ^ The origin address of the query.
278 -> Either err (IO (Maybe x))
279dispatchQuery (MethodHandler unwrapQ wrapR f) tid self x addr =
280 fmap (\a -> Just . wrapR tid self addr <$> f addr a) $ unwrapQ x
281dispatchQuery (NoReply unwrapQ f) tid self x addr =
282 fmap (\a -> f addr a >> return Nothing) $ unwrapQ x
283
284-- | These four parameters are required to implement an outgoing query. A
285-- peer-to-peer algorithm will define a 'MethodSerializer' for every 'MethodHandler' that
286-- might be returned by 'lookupHandler'.
287data MethodSerializer tid addr x meth a b = MethodSerializer
288 { -- | Returns the microseconds to wait for a response to this query being
289 -- sent to the given address. The /addr/ may also be modified to add
290 -- routing information.
291 methodTimeout :: tid -> addr -> STM (addr,Int)
292 -- | A method identifier used for error reporting. This needn't be the
293 -- same as the /meth/ argument to 'MethodHandler', but it is suggested.
294 , method :: meth
295 -- | Serialize the outgoing query /a/ into a transmittable packet /x/.
296 -- The /addr/ arguments are, respectively, our own origin address and the
297 -- destination of the request. The /tid/ argument is useful for attaching
298 -- auxiliary notations on all outgoing packets.
299 , wrapQuery :: tid -> addr -> addr -> a -> x
300 -- | Parse an inbound packet /x/ into a response /b/ for this query.
301 , unwrapResponse :: x -> b
302 }
303
304
305-- | To dispatch responses to our outbound queries, we require three
306-- primitives. See the 'transactionMethods' function to create these
307-- primitives out of a lookup table and a generator for transaction ids.
308--
309-- The type variable /d/ is used to represent the current state of the
310-- transaction generator and the table of pending transactions.
311data TransactionMethods d tid addr x = TransactionMethods
312 {
313 -- | Before a query is sent, this function stores an 'MVar' to which the
314 -- response will be written too. The returned /tid/ is a transaction id
315 -- that can be used to forget the 'MVar' if the remote peer is not
316 -- responding.
317 dispatchRegister :: MVar x -> addr -> d -> STM (tid, d)
318 -- | This method is invoked when an incoming packet /x/ indicates it is
319 -- a response to the transaction with id /tid/. The returned IO action
320 -- will write the packet to the correct 'MVar' thus completing the
321 -- dispatch.
322 , dispatchResponse :: tid -> x -> d -> STM (d, IO ())
323 -- | When a timeout interval elapses, this method is called to remove the
324 -- transaction from the table.
325 , dispatchCancel :: tid -> d -> STM d
326 }
327
328-- | The standard lookup table methods for use as input to 'transactionMethods'
329-- in lieu of directly implementing 'TransactionMethods'.
330data TableMethods t tid = TableMethods
331 { -- | Insert a new /tid/ entry into the transaction table.
332 tblInsert :: forall a. tid -> a -> t a -> t a
333 -- | Delete transaction /tid/ from the transaction table.
334 , tblDelete :: forall a. tid -> t a -> t a
335 -- | Lookup the value associated with transaction /tid/.
336 , tblLookup :: forall a. tid -> t a -> Maybe a
337 }
338
339-- | Methods for using 'Data.IntMap'.
340intMapMethods :: TableMethods IntMap Int
341intMapMethods = TableMethods IntMap.insert IntMap.delete IntMap.lookup
342
343-- | Methods for using 'Data.Word64Map'.
344w64MapMethods :: TableMethods Word64Map Word64
345w64MapMethods = TableMethods W64Map.insert W64Map.delete W64Map.lookup
346
347-- | Methods for using 'Data.Map'
348mapMethods :: Ord tid => TableMethods (Map tid) tid
349mapMethods = TableMethods Map.insert Map.delete Map.lookup
350
351-- | Change the key type for a lookup table implementation.
352--
353-- This can be used with 'intMapMethods' or 'mapMethods' to restrict lookups to
354-- only a part of the generated /tid/ value. This is useful for /tid/ types
355-- that are especially large due their use for other purposes, such as secure
356-- nonces for encryption.
357instance Contravariant (TableMethods t) where
358 -- contramap :: (tid -> t1) -> TableMethods t t1 -> TableMethods t tid
359 contramap f (TableMethods ins del lookup) =
360 TableMethods (\k v t -> ins (f k) v t)
361 (\k t -> del (f k) t)
362 (\k t -> lookup (f k) t)
363
364-- | Construct 'TransactionMethods' methods out of 3 lookup table primitives and a
365-- function for generating unique transaction ids.
366transactionMethods ::
367 TableMethods t tid -- ^ Table methods to lookup values by /tid/.
368 -> (g -> (tid,g)) -- ^ Generate a new unique /tid/ value and update the generator state /g/.
369 -> TransactionMethods (g,t (MVar x)) tid addr x
370transactionMethods (TableMethods insert delete lookup) generate = TransactionMethods
371 { dispatchCancel = \tid (g,t) -> return (g, delete tid t)
372 , dispatchRegister = \v _ (g,t) ->
373 let (tid,g') = generate g
374 t' = insert tid v t
375 in return ( tid, (g',t') )
376 , dispatchResponse = \tid x (g,t) ->
377 case lookup tid t of
378 Just v -> let t' = delete tid t
379 in return ((g,t'),void $ tryPutMVar v x)
380 Nothing -> return ((g,t), return ())
381 }
382
383-- | A set of methods necessary for dispatching incoming packets.
384data DispatchMethods tbl err meth tid addr x = DispatchMethods
385 { -- | Classify an inbound packet as a query or response.
386 classifyInbound :: x -> MessageClass err meth tid addr x
387 -- | Lookup the handler for a inbound query.
388 , lookupHandler :: meth -> Maybe (MethodHandler err tid addr x)
389 -- | Methods for handling incoming responses.
390 , tableMethods :: TransactionMethods tbl tid addr x
391 }
392
393-- | These methods indicate what should be done upon various conditions. Write
394-- to a log file, make debug prints, or simply ignore them.
395--
396-- [ /addr/ ] Address of remote peer.
397--
398-- [ /x/ ] Incoming or outgoing packet.
399--
400-- [ /meth/ ] Method id of incoming or outgoing request.
401--
402-- [ /tid/ ] Transaction id for outgoing packet.
403--
404-- [ /err/ ] Error information, typically a 'String'.
405data ErrorReporter addr x meth tid err = ErrorReporter
406 { -- | Incoming: failed to parse packet.
407 reportParseError :: err -> IO ()
408 -- | Incoming: no handler for request.
409 , reportMissingHandler :: meth -> addr -> x -> IO ()
410 -- | Incoming: unable to identify request.
411 , reportUnknown :: addr -> x -> err -> IO ()
412 -- | Outgoing: remote peer is not responding.
413 , reportTimeout :: meth -> tid -> addr -> IO ()
414 }
415
416ignoreErrors :: ErrorReporter addr x meth tid err
417ignoreErrors = ErrorReporter
418 { reportParseError = \_ -> return ()
419 , reportMissingHandler = \_ _ _ -> return ()
420 , reportUnknown = \_ _ _ -> return ()
421 , reportTimeout = \_ _ _ -> return ()
422 }
423
424printErrors :: ( Show addr
425 , Show meth
426 ) => Handle -> ErrorReporter addr x meth tid String
427printErrors h = ErrorReporter
428 { reportParseError = \err -> hPutStrLn h err
429 , reportMissingHandler = \meth addr x -> hPutStrLn h $ show addr ++ " --> Missing handler ("++show meth++")"
430 , reportUnknown = \addr x err -> hPutStrLn h $ show addr ++ " --> " ++ err
431 , reportTimeout = \meth tid addr -> hPutStrLn h $ show addr ++ " --> Timeout ("++show meth++")"
432 }
433
434-- Change the /err/ type for an 'ErrorReporter'.
435instance Contravariant (ErrorReporter addr x meth tid) where
436 -- contramap :: (t5 -> t4) -> ErrorReporter t3 t2 t1 t t4 -> ErrorReporter t3 t2 t1 t t5
437 contramap f (ErrorReporter pe mh unk tim)
438 = ErrorReporter (\e -> pe (f e))
439 mh
440 (\addr x e -> unk addr x (f e))
441 tim
442
443-- | Handle a single inbound packet and then invoke the given continuation.
444-- The 'forkListener' function is implemented by passing this function to 'fix'
445-- in a forked thread that loops until 'awaitMessage' returns 'Nothing' or
446-- throws an exception.
447handleMessage ::
448 Client err meth tid addr x
449 -> addr
450 -> x
451 -> IO (Maybe (x -> x))
452handleMessage (Client net d err pending whoami responseID) addr plain = do
453 -- Just (Left e) -> do reportParseError err e
454 -- return $! Just id
455 -- Just (Right (plain, addr)) -> do
456 case classifyInbound d plain of
457 IsQuery meth tid -> case lookupHandler d meth of
458 Nothing -> do reportMissingHandler err meth addr plain
459 return $! Just id
460 Just m -> do
461 self <- whoami (Just addr)
462 tid' <- responseID tid
463 either (\e -> do reportParseError err e
464 return $! Just id)
465 (>>= \m -> do mapM_ (sendMessage net addr) m
466 return $! Nothing)
467 (dispatchQuery m tid' self plain addr)
468 IsUnsolicited action -> do
469 self <- whoami (Just addr)
470 action self addr
471 return Nothing
472 IsResponse tid -> do
473 action <- atomically $ do
474 ts0 <- readTVar pending
475 (ts, action) <- dispatchResponse (tableMethods d) tid plain ts0
476 writeTVar pending ts
477 return action
478 action
479 return $! Nothing
480 IsUnknown e -> do reportUnknown err addr plain e
481 return $! Just id
482 -- Nothing -> return $! id
483
484-- * UDP Datagrams.
485
486-- | Access the address family of a given 'SockAddr'. This convenient accessor
487-- is missing from 'Network.Socket', so I implemented it here.
488sockAddrFamily :: SockAddr -> Family
489sockAddrFamily (SockAddrInet _ _ ) = AF_INET
490sockAddrFamily (SockAddrInet6 _ _ _ _) = AF_INET6
491sockAddrFamily (SockAddrUnix _ ) = AF_UNIX
492sockAddrFamily (SockAddrCan _ ) = AF_CAN
493
494-- | Packets with an empty payload may trigger EOF exception.
495-- 'udpTransport' uses this function to avoid throwing in that
496-- case.
497ignoreEOF :: a -> IOError -> IO a
498ignoreEOF def e | isEOFError e = pure def
499 | otherwise = throwIO e
500
501-- | Hard-coded maximum packet size for incoming UDP Packets received via
502-- 'udpTransport'.
503udpBufferSize :: Int
504udpBufferSize = 65536
505
506-- | Wrapper around 'B.sendTo' that silently ignores DoesNotExistError.
507saferSendTo :: Socket -> ByteString -> SockAddr -> IO ()
508saferSendTo sock bs saddr = void (B.sendTo sock bs saddr)
509 `catch` \e ->
510 -- sendTo: does not exist (Network is unreachable)
511 -- Occurs when IPv6 or IPv4 network is not available.
512 -- Currently, we require -threaded to prevent a forever-hang in this case.
513 if isDoesNotExistError e
514 then return ()
515 else throw e
516
517-- | A 'udpTransport' uses a UDP socket to send and receive 'ByteString's. The
518-- argument is the listen-address for incoming packets. This is a useful
519-- low-level 'Transport' that can be transformed for higher-level protocols
520-- using 'layerTransport'.
521udpTransport :: SockAddr -> IO (Transport err SockAddr ByteString)
522udpTransport bind_address = do
523 let family = sockAddrFamily bind_address
524 sock <- socket family Datagram defaultProtocol
525 when (family == AF_INET6) $ do
526 setSocketOption sock IPv6Only 0
527 bind sock bind_address
528 return Transport
529 { awaitMessage = \kont -> do
530 r <- handle (ignoreEOF $ Just $ Right (B.empty, SockAddrInet 0 0)) $ do
531 Just . Right <$!> B.recvFrom sock udpBufferSize
532 kont $! r
533 , sendMessage = case family of
534 AF_INET6 -> \case
535 (SockAddrInet port addr) -> \bs ->
536 -- Change IPv4 to 4mapped6 address.
537 saferSendTo sock bs $ SockAddrInet6 port 0 (0,0,0x0000ffff,fromBE32 addr) 0
538 addr6 -> \bs -> saferSendTo sock bs addr6
539 AF_INET -> \case
540 (SockAddrInet6 port 0 (0,0,0x0000ffff,raw4) 0) -> \bs -> do
541 let host4 = toBE32 raw4
542 -- Change 4mapped6 to ordinary IPv4.
543 -- hPutStrLn stderr $ "4mapped6 -> "++show (SockAddrInet port host4)
544 saferSendTo sock bs (SockAddrInet port host4)
545 addr@(SockAddrInet6 {}) -> \bs -> hPutStrLn stderr ("Discarding packet to "++show addr)
546 addr4 -> \bs -> saferSendTo sock bs addr4
547 _ -> \addr bs -> saferSendTo sock bs addr
548 , closeTransport = close sock
549 }
diff --git a/src/Network/SocketLike.hs b/src/Network/SocketLike.hs
new file mode 100644
index 00000000..d533dd7f
--- /dev/null
+++ b/src/Network/SocketLike.hs
@@ -0,0 +1,124 @@
1{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2{-# LANGUAGE CPP #-}
3-- |
4--
5-- A socket could be used indirectly via a 'System.IO.Handle' or a conduit from
6-- Michael Snoyman's conduit package. But doing so presents an encapsulation
7-- problem. Do we allow access to the underlying socket and trust that it wont
8-- be used in an unsafe way? Or do we protect it at the higher level and deny
9-- access to various state information?
10--
11-- The 'SocketLike' class enables the approach that provides a safe wrapper to
12-- the underlying socket and gives access to various state information without
13-- enabling direct reads or writes.
14module Network.SocketLike
15 ( SocketLike(..)
16 , RestrictedSocket
17 , restrictSocket
18 , restrictHandleSocket
19 -- * Re-exports
20 --
21 -- | To make the 'SocketLike' methods less awkward to use, the types
22 -- 'CUInt', 'SockAddr', and 'PortNumber' are re-exported.
23 , CUInt
24 , PortNumber
25 , SockAddr(..)
26 ) where
27
28import Network.Socket
29 ( PortNumber
30 , SockAddr
31 )
32import Foreign.C.Types ( CUInt )
33
34import qualified Network.Socket as NS
35import System.IO (Handle,hClose,hIsOpen)
36
37-- | A safe (mostly read-only) interface to a 'NS.Socket'. Note that despite
38-- how this class is named, it provides no access to typical 'NS.Socket' uses
39-- like sending or receiving network packets.
40class SocketLike sock where
41 -- | See 'NS.getSocketName'
42 getSocketName :: sock -> IO SockAddr
43 -- | See 'NS.getPeerName'
44 getPeerName :: sock -> IO SockAddr
45 -- | See 'NS.getPeerCred'
46 getPeerCred :: sock -> IO (CUInt, CUInt, CUInt)
47 -- | See 'NS.socketPort'
48 socketPort :: sock -> IO PortNumber
49 -- | See 'NS.sIsConnected'
50 --
51 -- __Warning__: Don't rely on this method if it's possible the socket was
52 -- converted into a 'Handle'.
53 sIsConnected :: sock -> IO Bool
54 -- | See 'NS.sIsBound'
55 sIsBound :: sock -> IO Bool
56 -- | See 'NS.sIsListening'
57 sIsListening :: sock -> IO Bool
58 -- | See 'NS.sIsReadable'
59 sIsReadable :: sock -> IO Bool
60 -- | See 'NS.sIsWritable'
61 sIsWritable :: sock -> IO Bool
62
63 -- | This is the only exposed write-access method to the
64 -- underlying state. Usually implemented by 'NS.close'
65 sClose :: sock -> IO ()
66
67instance SocketLike NS.Socket where
68 getSocketName = NS.getSocketName
69 getPeerName = NS.getPeerName
70 getPeerCred = NS.getPeerCred
71 socketPort = NS.socketPort
72#if MIN_VERSION_network(2,4,0)
73 sIsConnected = NS.isConnected -- warning: this is always False if the socket
74 -- was converted to a Handle
75 sIsBound = NS.isBound
76 sIsListening = NS.isListening
77 sIsReadable = NS.isReadable
78 sIsWritable = NS.isWritable
79 sClose = NS.close
80#else
81 sIsConnected = NS.sIsConnected -- warning: this is always False if the socket
82 -- was converted to a Handle
83 sIsBound = NS.sIsBound
84 sIsListening = NS.sIsListening
85 sIsReadable = NS.sIsReadable
86 sIsWritable = NS.sIsWritable
87 sClose = NS.sClose
88#endif
89
90
91-- | An encapsulated socket. Data reads and writes are not possible.
92data RestrictedSocket = Restricted (Maybe Handle) NS.Socket deriving Show
93
94instance SocketLike RestrictedSocket where
95 getSocketName (Restricted mb sock) = NS.getSocketName sock
96 getPeerName (Restricted mb sock) = NS.getPeerName sock
97 getPeerCred (Restricted mb sock) = NS.getPeerCred sock
98 socketPort (Restricted mb sock) = NS.socketPort sock
99#if MIN_VERSION_network(2,4,0)
100 sIsConnected (Restricted mb sock) = maybe (NS.isConnected sock) (hIsOpen) mb
101 sIsBound (Restricted mb sock) = NS.isBound sock
102 sIsListening (Restricted mb sock) = NS.isListening sock
103 sIsReadable (Restricted mb sock) = NS.isReadable sock
104 sIsWritable (Restricted mb sock) = NS.isWritable sock
105 sClose (Restricted mb sock) = maybe (NS.close sock) (\h -> hClose h >> NS.close sock) mb
106#else
107 sIsConnected (Restricted mb sock) = maybe (NS.sIsConnected sock) (hIsOpen) mb
108 sIsBound (Restricted mb sock) = NS.sIsBound sock
109 sIsListening (Restricted mb sock) = NS.sIsListening sock
110 sIsReadable (Restricted mb sock) = NS.sIsReadable sock
111 sIsWritable (Restricted mb sock) = NS.sIsWritable sock
112 sClose (Restricted mb sock) = maybe (NS.sClose sock) (\h -> hClose h >> NS.sClose sock) mb
113#endif
114
115-- | Create a 'RestrictedSocket' that explicitly disallows sending or
116-- receiving data.
117restrictSocket :: NS.Socket -> RestrictedSocket
118restrictSocket socket = Restricted Nothing socket
119
120-- | Build a 'RestrictedSocket' for which 'sClose' will close the given
121-- 'Handle'. It is intended that this 'Handle' was obtained via
122-- 'NS.socketToHandle'.
123restrictHandleSocket :: Handle -> NS.Socket -> RestrictedSocket
124restrictHandleSocket h socket = Restricted (Just h) socket
diff --git a/src/Network/StreamServer.hs b/src/Network/StreamServer.hs
new file mode 100644
index 00000000..34b9388e
--- /dev/null
+++ b/src/Network/StreamServer.hs
@@ -0,0 +1,153 @@
1-- | This module implements a bare-bones TCP or Unix socket server.
2{-# LANGUAGE CPP #-}
3{-# LANGUAGE TypeFamilies #-}
4{-# LANGUAGE TypeOperators #-}
5{-# LANGUAGE OverloadedStrings #-}
6{-# LANGUAGE RankNTypes #-}
7module Network.StreamServer
8 ( streamServer
9 , ServerHandle
10 , ServerConfig(..)
11 , withSession
12 , quitListening
13 , dummyServerHandle
14 ) where
15
16import Data.Monoid
17import Network.Socket as Socket
18import Data.ByteString.Char8
19 ( hGetNonBlocking
20 )
21import qualified Data.ByteString.Char8 as S
22 ( hPutStrLn
23 )
24import System.Directory (removeFile)
25import System.IO
26 ( IOMode(..)
27 , hSetBuffering
28 , BufferMode(..)
29 , hWaitForInput
30 , hClose
31 , hIsEOF
32 , hPutStrLn
33 , stderr
34 , hFlush
35 )
36import Control.Monad
37import Control.Monad.Fix (fix)
38#ifdef THREAD_DEBUG
39import Control.Concurrent.Lifted.Instrument (forkIO, threadDelay, ThreadId, mkWeakThreadId, labelThread, myThreadId)
40#else
41import GHC.Conc (labelThread)
42import Control.Concurrent (forkIO, threadDelay, ThreadId, mkWeakThreadId, myThreadId)
43#endif
44import Control.Exception (catch,handle,try,finally)
45import System.IO.Error (tryIOError)
46import System.Mem.Weak
47import System.IO.Error
48
49-- import Data.Conduit
50import Control.Monad.IO.Class (MonadIO (liftIO))
51import qualified Data.ByteString as S (ByteString)
52import System.IO (Handle)
53import Control.Concurrent.MVar (newMVar)
54
55import Network.SocketLike
56
57data ServerHandle = ServerHandle Socket (Weak ThreadId)
58
59
60-- | Create a useless do-nothing 'ServerHandle'.
61dummyServerHandle :: IO ServerHandle
62dummyServerHandle = do
63 mvar <- newMVar Closed
64 let sock = MkSocket 0 AF_UNSPEC NoSocketType 0 mvar
65 thread <- mkWeakThreadId <=< forkIO $ return ()
66 return (ServerHandle sock thread)
67
68removeSocketFile :: SockAddr -> IO ()
69removeSocketFile (SockAddrUnix fname) = removeFile fname
70removeSocketFile _ = return ()
71
72-- | Terminate the server accept-loop. Call this to shut down the server.
73quitListening :: ServerHandle -> IO ()
74quitListening (ServerHandle socket _) =
75 finally (Socket.getSocketName socket >>= removeSocketFile)
76 (Socket.close socket)
77
78
79-- | It's 'bshow' instead of 'show' to enable swapping in a 'ByteString'
80-- variation. (This is not exported.)
81bshow :: Show a => a -> String
82bshow e = show e
83
84-- | Send a string to stderr. Not exported. Default 'serverWarn' when
85-- 'withSession' is used to configure the server.
86warnStderr :: String -> IO ()
87warnStderr str = hPutStrLn stderr str >> hFlush stderr
88
89data ServerConfig = ServerConfig
90 { serverWarn :: String -> IO ()
91 -- ^ Action to report warnings and errors.
92 , serverSession :: RestrictedSocket -> Int -> Handle -> IO ()
93 -- ^ Action to handle interaction with a client
94 }
95
96-- | Initialize a 'ServerConfig' using the provided session handler.
97withSession :: (RestrictedSocket -> Int -> Handle -> IO ()) -> ServerConfig
98withSession session = ServerConfig warnStderr session
99
100-- | Launch a thread to listen at the given bind address and dispatch
101-- to session handler threads on every incomming connection. Supports
102-- IPv4 and IPv6, TCP and unix sockets.
103--
104-- The returned handle can be used with 'quitListening' to terminate the
105-- thread and prevent any new sessions from starting. Currently active
106-- session threads will not be terminated or signaled in any way.
107streamServer :: ServerConfig -> SockAddr -> IO ServerHandle
108streamServer cfg addr = do
109 let warn = serverWarn cfg
110 family = case addr of
111 SockAddrInet {} -> AF_INET
112 SockAddrInet6 {} -> AF_INET6
113 SockAddrUnix {} -> AF_UNIX
114 sock <- socket family Stream 0
115 setSocketOption sock ReuseAddr 1
116 fix $ \loop ->
117 tryIOError (removeSocketFile addr) >> bind sock addr
118 `catchIOError` \e -> do warn $ "bind-error: " <> bshow addr <> " " <> bshow e
119 threadDelay 5000000
120 loop
121 listen sock maxListenQueue
122 thread <- mkWeakThreadId <=< forkIO $ do
123 myThreadId >>= flip labelThread "StreamServer.acceptLoop"
124 acceptLoop cfg sock 0
125 return (ServerHandle sock thread)
126
127-- | Not exported. This, combined with 'acceptException' form a mutually recursive
128-- loop that handles incomming connections. To quit the loop, the socket must be
129-- closed by 'quitListening'.
130acceptLoop :: ServerConfig -> Socket -> Int -> IO ()
131acceptLoop cfg sock n = handle (acceptException cfg n sock) $ do
132 con <- accept sock
133 let conkey = n + 1
134 h <- socketToHandle (fst con) ReadWriteMode
135 forkIO $ do
136 myThreadId >>= flip labelThread "StreamServer.session"
137 serverSession cfg (restrictHandleSocket h (fst con)) conkey h
138 acceptLoop cfg sock (n + 1)
139
140acceptException :: ServerConfig -> Int -> Socket -> IOError -> IO ()
141acceptException cfg n sock ioerror = do
142 Socket.close sock
143 case show (ioeGetErrorType ioerror) of
144 "resource exhausted" -> do -- try again
145 serverWarn cfg $ ("acceptLoop: resource exhasted")
146 threadDelay 500000
147 acceptLoop cfg sock (n + 1)
148 "invalid argument" -> do -- quit on closed socket
149 return ()
150 message -> do -- unexpected exception
151 serverWarn cfg $ ("acceptLoop: "<>bshow message)
152 return ()
153
diff --git a/src/Network/Tox.hs b/src/Network/Tox.hs
new file mode 100644
index 00000000..37802e3c
--- /dev/null
+++ b/src/Network/Tox.hs
@@ -0,0 +1,369 @@
1{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveFoldable #-}
3{-# LANGUAGE DeriveFunctor #-}
4{-# LANGUAGE DeriveGeneric #-}
5{-# LANGUAGE DeriveTraversable #-}
6{-# LANGUAGE ExistentialQuantification #-}
7{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE GeneralizedNewtypeDeriving #-}
9{-# LANGUAGE LambdaCase #-}
10{-# LANGUAGE NamedFieldPuns #-}
11{-# LANGUAGE PatternSynonyms #-}
12{-# LANGUAGE RankNTypes #-}
13{-# LANGUAGE ScopedTypeVariables #-}
14{-# LANGUAGE TupleSections #-}
15module Network.Tox where
16
17import Debug.Trace
18import Control.Exception hiding (Handler)
19import Control.Applicative
20import Control.Arrow
21import Control.Concurrent (MVar)
22import Control.Concurrent.STM
23import Control.Monad
24import Control.Monad.Fix
25import qualified Crypto.Cipher.ChaChaPoly1305 as Symmetric
26import qualified Crypto.Cipher.Salsa as Salsa
27import qualified Crypto.Cipher.XSalsa as XSalsa
28import Crypto.ECC.Class
29import qualified Crypto.Error as Cryptonite
30import Crypto.Error.Types
31import qualified Crypto.MAC.Poly1305 as Poly1305
32import Crypto.PubKey.Curve25519
33import Crypto.PubKey.ECC.Types
34import Crypto.Random
35import qualified Data.Aeson as JSON
36 ;import Data.Aeson (FromJSON, ToJSON, (.=))
37import Data.Bitraversable (bisequence)
38import Data.Bits
39import Data.Bits.ByteString ()
40import Data.Bool
41import qualified Data.ByteArray as BA
42 ;import Data.ByteArray (ByteArrayAccess, Bytes)
43import qualified Data.ByteString as B
44 ;import Data.ByteString (ByteString)
45import qualified Data.ByteString.Base16 as Base16
46import qualified Data.ByteString.Char8 as C8
47import Data.ByteString.Lazy (toStrict)
48import Data.Char
49import Data.Data
50import Data.Functor.Contravariant
51import Data.Hashable
52import Data.IP
53import Data.Maybe
54import qualified Data.MinMaxPSQ as MinMaxPSQ
55 ;import Data.MinMaxPSQ (MinMaxPSQ')
56import Data.Monoid
57import Data.Ord
58import qualified Data.Serialize as S
59import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
60import Data.Typeable
61import Data.Word
62import qualified Data.Wrapper.PSQ as PSQ
63 ;import Data.Wrapper.PSQ (PSQ)
64import qualified Data.Wrapper.PSQInt as Int
65import Foreign.Marshal.Alloc
66import Foreign.Ptr
67import Foreign.Storable
68import GHC.Generics (Generic)
69import System.Global6
70import Network.Kademlia
71import Network.Address (Address, WantIP (..), either4or6,
72 fromSockAddr, ipFamily, setPort,
73 sockAddrPort, testIdBit,
74 toSockAddr, un4map)
75import Network.Kademlia.Search (Search (..))
76import qualified Network.Kademlia.Routing as R
77import Network.QueryResponse
78import Network.Socket
79import System.Endian
80import System.IO
81import qualified Text.ParserCombinators.ReadP as RP
82import Text.Printf
83import Text.Read
84import Control.TriadCommittee
85import Network.BitTorrent.DHT.Token as Token
86import GHC.TypeLits
87
88import Crypto.Tox
89import Data.Word64Map (fitsInInt)
90import qualified Data.Word64Map (empty)
91import Network.Kademlia.Bootstrap (forkPollForRefresh, bootstrap)
92import Network.Tox.Crypto.Transport (NetCrypto)
93import Network.Tox.Crypto.Handlers (cryptoNetHandler, newSessionsState, defaultUnRecHook, defaultCryptoDataHooks, NetCryptoSessions(..))
94import qualified Network.Tox.DHT.Handlers as DHT
95import qualified Network.Tox.DHT.Transport as DHT
96import Network.Tox.NodeId
97import qualified Network.Tox.Onion.Handlers as Onion
98import qualified Network.Tox.Onion.Transport as Onion
99import Network.Tox.Transport
100import OnionRouter
101import Roster
102import Text.XXD
103
104newCrypto :: IO TransportCrypto
105newCrypto = do
106 secret <- generateSecretKey
107 alias <- generateSecretKey
108 ralias <- generateSecretKey
109 let pubkey = toPublic secret
110 aliaspub = toPublic alias
111 raliaspub = toPublic ralias
112 ukeys <- atomically $ newTVar []
113 (symkey, drg) <- do
114 drg0 <- getSystemDRG
115 return $ randomBytesGenerate 32 drg0 :: IO (ByteString, SystemDRG)
116 noncevar <- atomically $ newTVar $ fst $ withDRG drg drgNew
117 cookieKeys <- atomically $ newTVar []
118 cache <- newSecretsCache
119 hPutStrLn stderr $ "secret(tox) = " ++ DHT.showHex secret
120 hPutStrLn stderr $ "public(tox) = " ++ DHT.showHex pubkey
121 hPutStrLn stderr $ "symmetric(tox) = " ++ DHT.showHex symkey
122 return TransportCrypto
123 { transportSecret = secret
124 , transportPublic = pubkey
125 , onionAliasSecret = alias
126 , onionAliasPublic = aliaspub
127 , rendezvousSecret = ralias
128 , rendezvousPublic = raliaspub
129 , transportSymmetric = return $ SymmetricKey symkey
130 , transportNewNonce = do
131 drg1 <- readTVar noncevar
132 let (nonce, drg2) = withDRG drg1 (Nonce24 <$> getRandomBytes 24)
133 writeTVar noncevar drg2
134 return nonce
135 , userKeys = ukeys
136 , pendingCookies = cookieKeys
137 , secretsCache = cache
138 }
139
140updateIP :: TVar (R.BucketList NodeInfo) -> SockAddr -> STM ()
141updateIP tblvar a = do
142 bkts <- readTVar tblvar
143 case nodeInfo (nodeId (R.thisNode bkts)) a of
144 Right ni -> writeTVar tblvar (bkts { R.thisNode = ni })
145 Left _ -> return ()
146
147genNonce24 :: DRG g =>
148 TVar (g, pending) -> DHT.TransactionId -> IO DHT.TransactionId
149genNonce24 var (DHT.TransactionId nonce8 _) = atomically $ do
150 (g,pending) <- readTVar var
151 let (bs, g') = randomBytesGenerate 24 g
152 writeTVar var (g',pending)
153 return $ DHT.TransactionId nonce8 (Nonce24 bs)
154
155
156gen :: forall gen. DRG gen => gen -> (DHT.TransactionId, gen)
157gen g = let (bs, g') = randomBytesGenerate 24 g
158 (ws, g'') = randomBytesGenerate 8 g'
159 Right w = S.runGet S.getWord64be ws
160 in ( DHT.TransactionId (Nonce8 w) (Nonce24 bs), g'' )
161
162intKey :: DHT.TransactionId -> Int
163intKey (DHT.TransactionId (Nonce8 w) _) = fromIntegral w
164
165w64Key :: DHT.TransactionId -> Word64
166w64Key (DHT.TransactionId (Nonce8 w) _) = w
167
168nonceKey :: DHT.TransactionId -> Nonce8
169nonceKey (DHT.TransactionId n _) = n
170
171-- | Return my own address.
172myAddr :: TVar (R.BucketList NodeInfo) -- ^ IPv4 buckets
173 -> TVar (R.BucketList NodeInfo) -- ^ IPv6 buckets
174 -> Maybe NodeInfo -- ^ Interested remote address
175 -> IO NodeInfo
176myAddr routing4 routing6 maddr = atomically $ do
177 let var = case flip DHT.prefer4or6 Nothing <$> maddr of
178 Just Want_IP6 -> routing4
179 _ -> routing6
180 a <- readTVar var
181 return $ R.thisNode a
182
183newClient :: (DRG g, Show addr, Show meth) =>
184 g -> Transport String addr x
185 -> (Client String meth DHT.TransactionId addr x -> x -> MessageClass String meth DHT.TransactionId addr x)
186 -> (Maybe addr -> IO addr)
187 -> (Client String meth DHT.TransactionId addr x -> meth -> Maybe (MethodHandler String DHT.TransactionId addr x))
188 -> (forall d. TransactionMethods d DHT.TransactionId addr x -> TransactionMethods d DHT.TransactionId addr x)
189 -> (Client String meth DHT.TransactionId addr x -> Transport String addr x -> Transport String addr x)
190 -> IO (Client String meth DHT.TransactionId addr x)
191newClient drg net classify selfAddr handlers modifytbl modifynet = do
192 -- If we have 8-byte keys for IntMap, then use it for transaction lookups.
193 -- Otherwise, use ordinary Map. The details of which will be hidden by an
194 -- existential closure (see mkclient below).
195 --
196 tblvar <-
197 if fitsInInt (Proxy :: Proxy Word64)
198 then do
199 let intmapT = transactionMethods (contramap intKey intMapMethods) gen
200 intmap_var <- atomically $ newTVar (drg, mempty)
201 return $ Right (intmapT,intmap_var)
202 else do
203 let word64mapT = transactionMethods (contramap w64Key w64MapMethods) gen
204 map_var <- atomically $ newTVar (drg, Data.Word64Map.empty)
205 return $ Left (word64mapT,map_var)
206 let dispatch tbl var handlers client = DispatchMethods
207 { classifyInbound = classify client
208 , lookupHandler = handlers -- var
209 , tableMethods = modifytbl tbl
210 }
211 eprinter = printErrors stderr
212 mkclient (tbl,var) handlers =
213 let client = Client
214 { clientNet = addHandler (reportParseError eprinter) (handleMessage client) $ modifynet client net
215 , clientDispatcher = dispatch tbl var (handlers client) client
216 , clientErrorReporter = eprinter { reportTimeout = reportTimeout ignoreErrors }
217 , clientPending = var
218 , clientAddress = selfAddr
219 , clientResponseId = genNonce24 var
220 }
221 in client
222 return $ either mkclient mkclient tblvar handlers
223
224data Tox = Tox
225 { toxDHT :: DHT.Client
226 , toxOnion :: Onion.Client RouteId
227 , toxToRoute :: Transport String Onion.AnnouncedRendezvous (PublicKey,Onion.OnionData)
228 , toxCrypto :: Transport String SockAddr NetCrypto
229 , toxCryptoSessions :: NetCryptoSessions
230 , toxCryptoKeys :: TransportCrypto
231 , toxRouting :: DHT.Routing
232 , toxTokens :: TVar SessionTokens
233 , toxAnnouncedKeys :: TVar Onion.AnnouncedKeys
234 , toxOnionRoutes :: OnionRouter
235 , toxRoster :: Roster
236 }
237
238getContactInfo :: Tox -> IO DHT.DHTPublicKey
239getContactInfo Tox{toxCryptoKeys,toxRouting} = join $ atomically $ do
240 r4 <- readTVar $ DHT.routing4 toxRouting
241 r6 <- readTVar $ DHT.routing6 toxRouting
242 nonce <- transportNewNonce toxCryptoKeys
243 let self = nodeId n4
244 n4 = R.thisNode r4
245 n6 = R.thisNode r6
246 n4s = R.kclosest DHT.toxSpace 4 self r4
247 n6s = R.kclosest DHT.toxSpace 4 self r6
248 ns = filter (DHT.isGlobal . nodeIP) [n4,n6]
249 ++ concat (zipWith (\a b -> [a,b]) n4s n6s)
250 return $ do
251 timestamp <- round . (* 1000000) <$> getPOSIXTime
252 return DHT.DHTPublicKey
253 { dhtpkNonce = timestamp
254 , dhtpk = id2key self
255 , dhtpkNodes = DHT.SendNodes $ take 4 ns
256 }
257
258isLocalHost :: SockAddr -> Bool
259isLocalHost (SockAddrInet _ host32) = (fromBE32 host32 == 0x7f000001)
260isLocalHost _ = False
261
262addVerbosity :: Transport err SockAddr ByteString -> Transport err SockAddr ByteString
263addVerbosity tr =
264 tr { awaitMessage = \kont -> awaitMessage tr $ \m -> do
265 forM_ m $ mapM_ $ \(msg,addr) -> do
266 when (not (B.null msg || elem (B.head msg) [0,1,2,4,0x81,0x82,0x8c,0x8d])) $ do
267 mapM_ (\x -> hPutStrLn stderr ( (show addr) ++ " --> " ++ x))
268 $ xxd 0 msg
269 kont m
270 , sendMessage = \addr msg -> do
271 when (not (B.null msg || elem (B.head msg) [0,1,2,4,0x81,0x8c,0x8d])) $ do
272 mapM_ (\x -> hPutStrLn stderr ( (show addr) ++ " <-- " ++ x))
273 $ xxd 0 msg
274 sendMessage tr addr msg
275 }
276
277newKeysDatabase :: IO (TVar Onion.AnnouncedKeys)
278newKeysDatabase =
279 atomically $ newTVar $ Onion.AnnouncedKeys PSQ.empty MinMaxPSQ.empty
280
281
282getOnionAlias :: TransportCrypto -> STM NodeInfo -> Maybe (Onion.OnionDestination r) -> IO (Onion.OnionDestination r)
283getOnionAlias crypto dhtself remoteNode = atomically $ do
284 ni <- dhtself
285 let alias = case remoteNode of
286 Just (Onion.OnionDestination (Onion.AnnouncingAlias _ uk) _ _)
287 -> ni { nodeId = key2id uk }
288 _ -> ni { nodeId = key2id (onionAliasPublic crypto) }
289 return $ Onion.OnionDestination Onion.SearchingAlias alias Nothing
290
291
292newTox :: TVar Onion.AnnouncedKeys -> SockAddr -> Maybe NetCryptoSessions -> Maybe SecretKey -> IO Tox
293newTox keydb addr mbSessionsState suppliedDHTKey = do
294 udp <- {- addVerbosity <$> -} udpTransport addr
295 (crypto0,sessionsState) <- case mbSessionsState of
296 Nothing -> do
297 crypto <- newCrypto
298 sessionsState <- newSessionsState crypto defaultUnRecHook defaultCryptoDataHooks
299 return (crypto,sessionsState)
300 Just s -> return (transportCrypto s, s)
301
302 let crypto = fromMaybe crypto0 $do
303 k <- suppliedDHTKey
304 return crypto0
305 { transportSecret = k
306 , transportPublic = toPublic k
307 }
308 forM_ suppliedDHTKey $ \k -> do
309 maybe (hPutStrLn stderr "failed to encode suppliedDHTKey")
310 (C8.hPutStrLn stderr . C8.append "Using suppliedDHTKey: ")
311 $ encodeSecret k
312
313 drg <- drgNew
314 let lookupClose _ = return Nothing
315
316 mkrouting <- DHT.newRouting addr crypto updateIP updateIP
317 let ignoreErrors _ = return () -- Set this to (hPutStrLn stderr) to debug onion route building.
318 orouter <- newOnionRouter ignoreErrors
319 (dhtcrypt,onioncrypt,dtacrypt,cryptonet) <- toxTransport crypto orouter lookupClose udp
320 let dhtnet0 = layerTransportM (DHT.decrypt crypto) (DHT.encrypt crypto) dhtcrypt
321 tbl4 = DHT.routing4 $ mkrouting (error "missing client")
322 tbl6 = DHT.routing6 $ mkrouting (error "missing client")
323 dhtclient <- newClient drg dhtnet0 DHT.classify (myAddr tbl4 tbl6) (DHT.handlers crypto . mkrouting) id
324 $ \client net -> onInbound (DHT.updateRouting client (mkrouting client) orouter) net
325
326 orouter <- forkRouteBuilder orouter $ \nid ni -> fmap (\(_,ns,_)->ns) <$> DHT.getNodes dhtclient nid ni
327
328 toks <- do
329 nil <- nullSessionTokens
330 atomically $ newTVar nil { maxInterval = 20 } -- 20 second timeout on announce ping-ids.
331 oniondrg <- drgNew
332 let onionnet = layerTransportM (Onion.decrypt crypto) (Onion.encrypt crypto) onioncrypt
333 onionclient <- newClient oniondrg onionnet (const Onion.classify)
334 (getOnionAlias crypto $ R.thisNode <$> readTVar (DHT.routing4 $ mkrouting dhtclient))
335 (const $ Onion.handlers onionnet (mkrouting dhtclient) toks keydb)
336 (hookQueries orouter DHT.transactionKey)
337 (const id)
338
339 roster <- newRoster
340 return Tox
341 { toxDHT = dhtclient
342 , toxOnion = onionclient
343 , toxToRoute = onInbound (updateRoster roster) dtacrypt
344 , toxCrypto = addHandler (hPutStrLn stderr) (cryptoNetHandler sessionsState) cryptonet
345 , toxCryptoSessions = sessionsState
346 , toxCryptoKeys = crypto
347 , toxRouting = mkrouting dhtclient
348 , toxTokens = toks
349 , toxAnnouncedKeys = keydb
350 , toxOnionRoutes = orouter
351 , toxRoster = roster
352 }
353
354onionTimeout :: Tox -> DHT.TransactionId -> Onion.OnionDestination RouteId -> STM (Onion.OnionDestination RouteId, Int)
355onionTimeout Tox { toxOnionRoutes = or } (DHT.TransactionId n8 _) od = lookupTimeout or n8 od
356
357forkTox :: Tox -> IO (IO (), [NodeInfo] -> [NodeInfo] -> IO (), [NodeInfo] -> [NodeInfo] -> IO ())
358forkTox tox = do
359 _ <- forkListener "toxCrypto" (toxCrypto tox)
360 _ <- forkListener "toxToRoute" (toxToRoute tox)
361 _ <- forkListener "toxOnion" (clientNet $ toxOnion tox)
362 quit <- forkListener "toxDHT" (clientNet $ toxDHT tox)
363 forkPollForRefresh (DHT.refresher4 $ toxRouting tox)
364 forkPollForRefresh (DHT.refresher6 $ toxRouting tox)
365 return ( quit
366 , bootstrap (DHT.refresher4 $ toxRouting tox)
367 , bootstrap (DHT.refresher6 $ toxRouting tox)
368 )
369
diff --git a/src/Network/Tox/Crypto/Handlers.hs b/src/Network/Tox/Crypto/Handlers.hs
new file mode 100644
index 00000000..ac3d1ef0
--- /dev/null
+++ b/src/Network/Tox/Crypto/Handlers.hs
@@ -0,0 +1,285 @@
1{-# LANGUAGE NamedFieldPuns #-}
2{-# LANGUAGE TupleSections #-}
3module Network.Tox.Crypto.Handlers where
4
5import Network.Tox.Crypto.Transport
6import Network.Tox.DHT.Transport (Cookie(..),CookieData(..))
7import Crypto.Tox
8import Control.Concurrent.STM
9import Network.Address
10import qualified Data.Map.Strict as Map
11import Crypto.Hash
12import Control.Applicative
13import Control.Monad
14import Data.Time.Clock.POSIX
15import qualified Data.ByteString as B
16import Control.Lens
17import Data.Function
18import Data.Serialize as S
19import Data.Word
20import GHC.Conc (unsafeIOToSTM)
21import qualified Data.Set as Set
22
23-- util, todo: move to another module
24maybeToEither :: Maybe b -> Either String b
25maybeToEither (Just x) = Right x
26maybeToEither Nothing = Left "maybeToEither"
27
28data NetCryptoSessionStatus = Unaccepted | Accepted | Confirmed
29 deriving (Eq,Ord,Show,Enum)
30
31
32type IOHook addr x = addr -> x -> IO (Maybe (x -> x))
33type NetCryptoHook = IOHook NetCryptoSession CryptoData
34
35
36data NetCryptoSession = NCrypto { ncState :: TVar NetCryptoSessionStatus
37 , ncTheirBaseNonce :: TVar Nonce24 -- base nonce + packet number
38 , ncMyPacketNonce :: TVar Nonce24 -- base nonce + packet number
39 , ncHandShake :: TVar (Maybe (Handshake Encrypted))
40 , ncCookie :: TVar (Maybe Cookie)
41 , ncTheirDHTKey :: PublicKey
42 , ncTheirSessionPublic :: Maybe PublicKey
43 , ncSessionSecret :: SecretKey
44 , ncSockAddr :: SockAddr
45 , ncHooks :: TVar (Map.Map MessageType [NetCryptoHook])
46 , ncUnrecognizedHook :: TVar (MessageType -> NetCryptoHook)
47 , ncAllSessions :: NetCryptoSessions -- ^ may be needed if one net-crypto session
48 -- needs to possibly start another, as is
49 -- the case in group chats
50 , ncGroups :: TVar (Map.Map GroupChatId (Set.Set SockAddr))
51 }
52
53data NetCryptoSessions = NCSessions { netCryptoSessions :: TVar (Map.Map SockAddr NetCryptoSession)
54 , transportCrypto :: TransportCrypto
55 , defaultHooks :: Map.Map MessageType [NetCryptoHook]
56 , defaultUnrecognizedHook :: MessageType -> NetCryptoHook
57 }
58
59newSessionsState :: TransportCrypto -> (MessageType -> NetCryptoHook) -> Map.Map MessageType [NetCryptoHook] -> IO NetCryptoSessions
60newSessionsState crypto unrechook hooks = do
61 x <- atomically $ newTVar Map.empty
62 return NCSessions { netCryptoSessions = x
63 , transportCrypto = crypto
64 , defaultHooks = hooks
65 , defaultUnrecognizedHook = unrechook
66 }
67
68data HandshakeParams
69 = HParam
70 { hpTheirBaseNonce :: Maybe Nonce24 -- ignore and generate your own
71 , hpOtherCookie :: Maybe Cookie
72 , hpTheirSessionKeyPublic :: PublicKey
73 , hpMySecretKey :: SecretKey
74 , hpCookieRemotePubkey :: PublicKey
75 , hpCookieRemoteDhtkey :: PublicKey
76 }
77newHandShakeData :: TransportCrypto -> HandshakeParams -> HandshakeData
78newHandShakeData = error "todo"
79
80-- | called when we recieve a crypto handshake with valid cookie
81freshCryptoSession :: NetCryptoSessions -> SockAddr -> HandshakeParams -> IO ()
82freshCryptoSession sessions
83 addr
84 hp@(HParam
85 { hpTheirBaseNonce = Just theirBaseNonce
86 , hpOtherCookie = Just otherCookie
87 , hpTheirSessionKeyPublic = theirSessionKey
88 , hpMySecretKey = key
89 , hpCookieRemotePubkey = remotePublicKey
90 , hpCookieRemoteDhtkey = remoteDhtPublicKey
91 }) = do
92 let crypto = transportCrypto sessions
93 allsessions = netCryptoSessions sessions
94 ncState0 <- atomically $ newTVar Accepted
95 ncTheirBaseNonce0 <- atomically $ newTVar theirBaseNonce
96 n24 <- atomically $ transportNewNonce crypto
97 state <- lookupSharedSecret crypto key remoteDhtPublicKey n24
98 let myhandshakeData = newHandShakeData crypto hp
99 plain = encodePlain myhandshakeData
100 encrypted = encrypt state plain
101 myhandshake = Handshake { handshakeCookie = otherCookie
102 , handshakeNonce = n24
103 , handshakeData = encrypted
104 }
105 ncMyPacketNonce0 <- atomically $ newTVar (baseNonce myhandshakeData)
106 ncHandShake0 <- atomically $ newTVar (Just myhandshake)
107 cookie0 <- atomically $ newTVar (Just otherCookie)
108 newsession <- generateSecretKey
109 ncHooks0 <- atomically $ newTVar (defaultHooks sessions)
110 ncUnrecognizedHook0 <- atomically $ newTVar (defaultUnrecognizedHook sessions)
111 ncGroups0 <- atomically $ newTVar (Map.empty)
112 let netCryptoSession =
113 NCrypto { ncState = ncState0
114 , ncTheirBaseNonce= ncTheirBaseNonce0
115 , ncMyPacketNonce = ncMyPacketNonce0
116 , ncHandShake = ncHandShake0
117 , ncCookie = cookie0
118 , ncTheirDHTKey = remoteDhtPublicKey
119 , ncTheirSessionPublic = Just theirSessionKey
120 , ncSessionSecret = newsession
121 , ncSockAddr = addr
122 , ncHooks = ncHooks0
123 , ncUnrecognizedHook = ncUnrecognizedHook0
124 , ncAllSessions = sessions
125 , ncGroups = ncGroups0
126 }
127 atomically $ modifyTVar allsessions (Map.insert addr netCryptoSession)
128
129-- | Called when we get a handshake, but there's already a session entry.
130updateCryptoSession :: NetCryptoSessions -> SockAddr -> HandshakeParams -> NetCryptoSession -> IO ()
131updateCryptoSession sessions addr hp session = do
132 ncState0 <- atomically $ readTVar (ncState session)
133 ncTheirBaseNonce0 <- atomically $ readTVar (ncTheirBaseNonce session)
134 if (ncState0 >= Accepted)
135 -- If the nonce in the handshake and the dht key are both the same as
136 -- the ones we have saved, assume we already handled this and this is a
137 -- duplicate handshake packet, otherwise disregard everything, and
138 -- refresh all state.
139 --
140 then when ( Just ncTheirBaseNonce0 /= hpTheirBaseNonce hp
141 || ncTheirDHTKey session /= hpCookieRemoteDhtkey hp
142 ) $ freshCryptoSession sessions addr hp
143 else if ( Just ncTheirBaseNonce0 /= hpTheirBaseNonce hp)
144 then freshCryptoSession sessions addr hp -- basenonce mismatch, trigger refresh
145 else atomically $ writeTVar (ncState session) Accepted
146
147
148cryptoNetHandler :: NetCryptoSessions -> SockAddr -> NetCrypto -> IO (Maybe (NetCrypto -> NetCrypto))
149cryptoNetHandler sessions addr (NetHandshake (Handshake (Cookie n24 ecookie) nonce24 encrypted)) = do
150 -- Handle Handshake Message
151 let crypto = transportCrypto sessions
152 allsessions = netCryptoSessions sessions
153 anyRight [] f = return $ Left "missing key"
154 anyRight (x:xs) f = f x >>= either (const $ anyRight xs f) (return . Right)
155 seckeys <- map fst <$> atomically (readTVar (userKeys crypto))
156 symkey <- atomically $ transportSymmetric crypto
157 now <- getPOSIXTime
158 lr <- fmap join . sequence $ do -- Either Monad
159 (CookieData cookieTime remotePubkey remoteDhtkey) <- (decodePlain =<< decryptSymmetric symkey n24 ecookie)
160 Right $ do -- IO Monad
161 decrypted <- anyRight seckeys $ \key -> do
162 secret <- lookupSharedSecret crypto key remotePubkey nonce24
163 return $ (key,) <$> (decodePlain =<< decrypt secret encrypted)
164 return $ do -- Either Monad
165 (key,HandshakeData { baseNonce, sessionKey, cookieHash, otherCookie }) <- decrypted
166 -- check cookie time < 15 seconds ago
167 guard (now - fromIntegral cookieTime < 15)
168 -- cookie hash is valid? sha512 of ecookie
169 let hinit = hashInit
170 hctx = hashUpdate hinit n24
171 hctx' = hashUpdate hctx ecookie
172 digest = hashFinalize hctx'
173 guard (cookieHash == digest)
174 -- known friend?
175 -- todo
176 return
177 HParam
178 { hpTheirBaseNonce = Just baseNonce
179 , hpOtherCookie = Just otherCookie
180 , hpTheirSessionKeyPublic = sessionKey
181 , hpMySecretKey = key
182 , hpCookieRemotePubkey = remotePubkey
183 , hpCookieRemoteDhtkey = remoteDhtkey
184 }
185 case lr of
186 Left _ -> return ()
187 Right hp@(HParam
188 { hpTheirBaseNonce = Just theirBaseNonce
189 , hpOtherCookie = Just otherCookie
190 , hpTheirSessionKeyPublic = theirSessionKey
191 , hpMySecretKey = key
192 , hpCookieRemotePubkey = remotePublicKey
193 , hpCookieRemoteDhtkey = remoteDhtPublicKey
194 }) -> do
195 sessionsmap <- atomically $ readTVar allsessions
196 -- Do a lookup, so we can handle the update case differently
197 case Map.lookup addr sessionsmap of
198 Nothing -> freshCryptoSession sessions addr hp -- create new session
199 Just session -> updateCryptoSession sessions addr hp session -- update existing session
200 return Nothing
201
202
203cryptoNetHandler sessions addr (NetCrypto (CryptoPacket nonce16 encrypted)) = do
204 let crypto = transportCrypto sessions
205 allsessions = netCryptoSessions sessions
206 sessionsmap <- atomically $ readTVar allsessions
207 -- Handle Encrypted Message
208 case Map.lookup addr sessionsmap of
209 Nothing -> return Nothing -- drop packet, we have no session
210 Just session@(NCrypto {ncState, ncHooks,ncSessionSecret,ncTheirSessionPublic,ncTheirBaseNonce}) -> do
211 theirBaseNonce <- atomically $ readTVar ncTheirBaseNonce
212 -- Try to decrypt message
213 let diff :: Word16
214 diff = nonce16 - fromIntegral (last2Bytes theirBaseNonce) -- truncating to Word16
215 tempNonce <- addtoNonce24 theirBaseNonce (fromIntegral diff) -- expanding to Word
216 lr <- fmap join $ sequence $ do -- Either Monad --
217 pubkey <- maybeToEither ncTheirSessionPublic
218 Right $ do -- IO Monad
219 secret <- lookupSharedSecret crypto ncSessionSecret pubkey tempNonce
220 return $ decodePlain =<< decrypt secret encrypted
221 case lr of
222 Left _ -> return Nothing -- decryption failed, ignore packet
223 Right cd@(CryptoData {bufferStart, bufferEnd, bufferData=cm}) -> do -- decryption succeeded,
224 -- TODO: Why do I need bufferStart & bufferEnd?
225 --
226 -- buffer_start = highest packet number handled + 1
227 -- , recvbuffers buffer_start
228 --
229 -- bufferEnd = sendbuffer buffer_end if lossy, otherwise packet number
230 -- update ncTheirBaseNonce if necessary
231 when (diff > 2 * dATA_NUM_THRESHOLD)$
232 atomically $ do
233 y <- readTVar ncTheirBaseNonce
234 -- all because Storable forces IO...
235 x <- unsafeIOToSTM $ addtoNonce24 y (fromIntegral dATA_NUM_THRESHOLD)
236 writeTVar ncTheirBaseNonce y
237 -- then set session confirmed,
238 atomically $ writeTVar ncState Confirmed
239 hookmap <- atomically $ readTVar ncHooks
240 -- run hook
241 flip fix (cd,hookmap) $ \lookupAgain (cd,hookmap) -> do
242 let msgTyp = cd ^. messageType
243 case Map.lookup msgTyp hookmap of
244 Nothing -> do -- no recognizing hook, run ncUnrecognizedHook0, loopAgain on result
245 unrecognize <- atomically $ readTVar (ncUnrecognizedHook session)
246 mbConsume <- unrecognize msgTyp session cd
247 case mbConsume of
248 Just f -> do
249 -- ncUnrecognizedHook0 may have updated the hookmap
250 hookmap' <- atomically $ readTVar ncHooks
251 lookupAgain (f cd,hookmap')
252 Nothing -> return Nothing
253 Just hooks -> flip fix (hooks,cd,msgTyp) $ \loop (hooks,cd,typ) -> do
254 let _ = cd :: CryptoData
255 case (hooks,cd) of
256 ([],_) -> return Nothing
257 (hook:more,cd) -> do
258 r <- hook session cd :: IO (Maybe (CryptoData -> CryptoData))
259 case r of
260 Just f -> let newcd = f cd
261 newtyp = newcd ^. messageType
262 in if newtyp == typ then loop (more,newcd,newtyp)
263 else lookupAgain (newcd,hookmap)
264 Nothing -> return Nothing -- message consumed
265 where
266 last2Bytes :: Nonce24 -> Word
267 last2Bytes (Nonce24 bs) = case S.decode (B.drop 22 bs) of
268 Right n -> n
269 _ -> error "unreachable-last2Bytes"
270 dATA_NUM_THRESHOLD = 21845 -- = 65535 / 3
271
272-- | handles nothing
273defaultCryptoDataHooks :: Map.Map MessageType [NetCryptoHook]
274defaultCryptoDataHooks = Map.empty
275
276-- | discards all unrecognized packets
277defaultUnRecHook :: MessageType -> NetCryptoHook
278defaultUnRecHook _ _ _ = return Nothing
279
280-- | use to add a single hook to a specific session.
281addCryptoDataHook1 :: Map.Map MessageType [NetCryptoHook] -> MessageType -> NetCryptoHook -> Map.Map MessageType [NetCryptoHook]
282addCryptoDataHook1 mp typ hook = case Map.lookup typ mp of
283 Nothing -> Map.insert typ [hook] mp
284 Just hooks -> Map.insert typ (hook:hooks) mp
285
diff --git a/src/Network/Tox/Crypto/Transport.hs b/src/Network/Tox/Crypto/Transport.hs
new file mode 100644
index 00000000..8739c853
--- /dev/null
+++ b/src/Network/Tox/Crypto/Transport.hs
@@ -0,0 +1,1172 @@
1{-# LANGUAGE KindSignatures #-}
2{-# LANGUAGE NamedFieldPuns #-}
3{-# LANGUAGE ViewPatterns #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE LambdaCase #-}
6{-# LANGUAGE TupleSections #-}
7module Network.Tox.Crypto.Transport
8 ( parseNetCrypto
9 , encodeNetCrypto
10 -- CryptoTransport
11 , NetCrypto(..)
12 , CryptoData(..)
13 , CryptoMessage(..)
14 , MessageName(..)
15 , CryptoPacket(..)
16 , HandshakeData(..)
17 , Handshake(..)
18 , PeerInfo(..)
19 , MessageID(..)
20 , UserStatus(..)
21 , TypingStatus(..)
22 , GroupChatId(..)
23 , MessageType(..)
24 -- feild name classes
25 , HasGroupChatID(..)
26 , HasGroupNumber(..)
27 , HasPeerNumber(..)
28 , HasMessageNumber(..)
29 , HasMessageName(..)
30 , HasMessageData(..)
31 , HasName(..)
32 , HasTitle(..)
33 , HasMessage(..)
34 , HasMessageType(..)
35 -- lenses
36 , userStatus, nick, statusMessage, typingStatus, action, groupChatID
37 , groupNumber, groupNumberToJoin, peerNumber, messageNumber
38 , messageName, messageData, name, title, message, messageType
39 -- constructor
40 , msg
41 , leaveMsg
42 , peerQueryMsg
43 -- utils
44 , sizedN
45 , sizedAtLeastN
46 , isIndirectGrpChat
47 , LossyOrLossless(..)
48 , lossyness
49 ) where
50
51import Crypto.Tox
52import Network.Tox.DHT.Transport (Cookie)
53import Network.Tox.NodeId
54
55import Network.Socket
56import Data.ByteArray
57
58import Data.ByteString as B
59import Data.Maybe
60import Data.Monoid
61import Data.Word
62import Crypto.Hash
63import Control.Lens
64import Data.Text as T
65import Data.Text.Encoding as T
66import Data.Serialize as S
67import Control.Arrow
68
69
70data NetCrypto
71 = NetHandshake (Handshake Encrypted)
72 | NetCrypto (CryptoPacket Encrypted)
73
74parseNetCrypto :: ByteString -> SockAddr -> Either String (NetCrypto, SockAddr)
75parseNetCrypto pkt@(B.uncons -> Just (0x1a,_)) saddr = left ("parseNetCrypto: "++) $ (,saddr) . NetHandshake <$> runGet get pkt
76parseNetCrypto pkt@(B.uncons -> Just (0x1b,_)) saddr = left ("parseNetCrypto: "++) $ (,saddr) . NetCrypto <$> runGet get pkt
77parseNetCrypto _ _ = Left "parseNetCrypto: ?"
78
79encodeNetCrypto :: NetCrypto -> SockAddr -> (ByteString, SockAddr)
80encodeNetCrypto (NetHandshake x) saddr = (B.cons 0x1a (runPut $ put x),saddr)
81encodeNetCrypto (NetCrypto x) saddr = (B.cons 0x1b (runPut $ put x),saddr)
82
83data Handshake (f :: * -> *) = Handshake
84 { -- The cookie is a cookie obtained by
85 -- sending a cookie request packet to the peer and getting a cookie
86 -- response packet with a cookie in it. It may also be obtained in the
87 -- handshake packet by a peer receiving a handshake packet (Other
88 -- Cookie).
89 handshakeCookie :: Cookie
90 -- The nonce is a nonce used to encrypt the encrypted part of the handshake
91 -- packet.
92 , handshakeNonce :: Nonce24
93 -- The encrypted part of the handshake packet is encrypted with the long
94 -- term user-keys of both peers.
95 , handshakeData :: f HandshakeData
96 }
97
98instance Serialize (Handshake Encrypted) where
99 get = Handshake <$> get <*> get <*> get
100 put (Handshake cookie n24 dta) = put cookie >> put n24 >> put dta
101
102data HandshakeData = HandshakeData
103 { baseNonce :: Nonce24
104 , sessionKey :: PublicKey
105 , cookieHash :: Digest SHA512
106 , otherCookie :: Cookie
107 }
108
109instance Sized HandshakeData where
110 size = contramap baseNonce size
111 <> contramap (key2id . sessionKey) size
112 <> ConstSize 64 -- contramap cookieHash size -- missing instance Sized (Digest SHA512)
113 <> contramap otherCookie size
114
115instance Serialize HandshakeData where
116 get = HandshakeData <$> get
117 <*> (id2key <$> get)
118 <*> (fromJust . digestFromByteString <$> getBytes 64)
119 <*> get
120 put (HandshakeData n k h c) = do
121 put n
122 put $ key2id k
123 putByteString (convert h)
124 put c
125
126data CryptoPacket (f :: * -> *) = CryptoPacket
127 { -- | The last 2 bytes of the nonce used to encrypt 'pktData'
128 pktNonce :: Word16
129 -- The payload is encrypted with the session key and 'baseNonce' set by
130 -- the receiver in their handshake + packet number (starting at 0, big
131 -- endian math).
132 , pktData :: f CryptoData
133 }
134
135instance Sized CryptoData where
136 size = contramap bufferStart size
137 <> contramap bufferEnd size
138 <> contramap bufferData size
139
140instance Serialize (CryptoPacket Encrypted) where
141 get = CryptoPacket <$> get <*> get
142 put (CryptoPacket n16 dta) = put n16 >> put dta
143
144data CryptoData = CryptoData
145 { -- | [our recvbuffers buffer_start, (highest packet number handled + 1), (big endian)]
146 bufferStart :: Word32
147 -- | [ uint32_t packet number if lossless
148 -- , sendbuffer buffer_end if lossy , (big endian)]
149 , bufferEnd :: Word32
150 -- | [data]
151 , bufferData :: CryptoMessage
152 }
153
154instance Serialize CryptoData where
155 get = CryptoData <$> get <*> get <*> get
156 put (CryptoData start end dta) = put start >> put end >> put dta
157
158-- The 'UserStatus' equivalent in Presence is:
159--
160-- data JabberShow = Offline
161-- | ExtendedAway
162-- | Away -- Tox equiv: Away (1)
163-- | DoNotDisturb -- Tox equiv: Busy (2)
164-- | Available -- Tox equiv: Online (0)
165-- | Chatty
166-- deriving (Show,Enum,Ord,Eq,Read)
167--
168-- The Enum instance on 'UserStatus' is not arbitrary. It corresponds
169-- to on-the-wire id numbers.
170data UserStatus = Online | Away | Busy deriving (Show,Eq,Ord,Enum)
171
172data TypingStatus = NotTyping | Typing deriving (Show,Eq,Ord,Enum)
173
174data CryptoMessage
175 = OneByte { msgID :: MessageID }
176 | TwoByte { msgID :: MessageID, msgByte :: Word8 }
177 | UpToN { msgID :: MessageID, msgBytes :: ByteString } -- length < N
178 deriving (Eq,Show)
179
180instance Sized CryptoMessage where
181 size = VarSize $ \case
182 OneByte {} -> 1
183 TwoByte {} -> 2
184 UpToN { msgBytes = bs } -> 1 + B.length bs
185
186instance Serialize CryptoMessage where
187 get = do
188 i <- get :: Get MessageID
189 n <- remaining
190 case msgSizeParam i of
191 Just (True,1) -> return $ OneByte i
192 Just (True,2) -> TwoByte i <$> get
193 _ -> UpToN i <$> getByteString n
194
195 put (OneByte i) = putWord8 (fromIntegral . fromEnum $ i)
196 put (TwoByte i b) = do putWord8 (fromIntegral . fromEnum $ i)
197 putWord8 b
198 put (UpToN i x) = do putWord8 (fromIntegral . fromEnum $ i)
199 putByteString x
200
201instance Serialize MessageID where
202 get = toEnum . fromIntegral <$> getWord8
203 put x = putWord8 (fromIntegral . fromEnum $ x)
204
205erCompat :: String -> a
206erCompat lens = error $ "Use of '" ++ lens ++ "' lens on incompatible CryptoMessage type"
207
208typingStatus :: Functor f => (UserStatus -> f UserStatus)-> (CryptoMessage -> f CryptoMessage)
209typingStatus = lens getter setter
210 where
211 getter :: CryptoMessage -> UserStatus
212 getter (TwoByte TYPING status) = toEnum $ fromIntegral status
213 getter _ = erCompat "typingStatus"
214 setter :: CryptoMessage -> UserStatus -> CryptoMessage
215 setter (TwoByte TYPING _) status = TwoByte TYPING (fromIntegral . fromEnum $ status)
216 setter _ _ = erCompat "typingStatus"
217
218userStatus :: Functor f => (UserStatus -> f UserStatus)-> (CryptoMessage -> f CryptoMessage)
219userStatus = lens getter setter
220 where
221 getter (TwoByte USERSTATUS status) = toEnum $ fromIntegral status
222 getter _ = erCompat "userStatus"
223 setter (TwoByte USERSTATUS _) status = TwoByte USERSTATUS (fromIntegral . fromEnum $ status)
224 setter _ _ = erCompat "userStatus"
225
226nick :: Functor f => (Text -> f Text)-> (CryptoMessage -> f CryptoMessage)
227nick = lens getter setter
228 where
229 getter (UpToN NICKNAME bstr) = T.decodeUtf8 bstr
230 getter _ = erCompat "nick"
231 setter (UpToN NICKNAME _) nick = UpToN NICKNAME (T.encodeUtf8 $ nick)
232 setter _ _ = erCompat "nick"
233
234statusMessage :: Functor f => (String -> f String)-> (CryptoMessage -> f CryptoMessage)
235statusMessage = lens getter setter
236 where
237 getter (UpToN STATUSMESSAGE bstr) = T.unpack $ T.decodeUtf8 bstr
238 getter _ = erCompat "statusMessage"
239 setter (UpToN STATUSMESSAGE _) nick = UpToN STATUSMESSAGE (T.encodeUtf8 . T.pack $ nick)
240 setter _ _ = erCompat "statusMessage"
241
242action :: Functor f => (String -> f String)-> (CryptoMessage -> f CryptoMessage)
243action = lens getter setter
244 where
245 getter (UpToN ACTION bstr) = T.unpack $ T.decodeUtf8 bstr
246 getter _ = erCompat "action"
247 setter (UpToN ACTION _) action = UpToN ACTION (T.encodeUtf8 . T.pack $ action)
248 setter _ _ = erCompat "action"
249
250newtype GroupChatId = GrpId ByteString -- 33 bytes
251 deriving (Show,Eq)
252
253class HasGroupChatID x where
254 getGroupChatID :: x -> GroupChatId
255 setGroupChatID :: x -> GroupChatId -> x
256
257sizedN :: Int -> ByteString -> ByteString
258sizedN n bs = if B.length bs < n then B.append bs (B.replicate (n - B.length bs) 0)
259 else B.take n bs
260
261sizedAtLeastN :: Int -> ByteString -> ByteString
262sizedAtLeastN n bs = if B.length bs < n then B.append bs (B.replicate (n - B.length bs) 0)
263 else bs
264
265instance HasGroupChatID CryptoMessage where
266 -- Get
267 getGroupChatID (UpToN INVITE_GROUPCHAT payload)
268 = let (xs,ys) = B.splitAt 1 payload'
269 payload' = sizedN 38 payload
270 in case B.unpack xs of
271 [isResponse] | 0 <- isResponse -> GrpId (B.take 33 $ B.drop 2 ys) -- skip group number
272 [isResponse] | 1 <- isResponse -> GrpId (B.take 33 $ B.drop 4 ys) -- skip two group numbers
273 _ -> GrpId "" -- error "Unexpected value in INVITE_GROUPCHAT message"
274
275 getGroupChatID (UpToN ONLINE_PACKET payload) = GrpId (B.take 33 $ B.drop 2 (sizedN 35 payload))
276 getGroupChatID _ = error "getGroupChatID on non-groupchat message."
277
278 -- Set
279 setGroupChatID msg@(UpToN INVITE_GROUPCHAT payload) (GrpId newid)
280 = let (xs,ys) = B.splitAt 1 payload'
281 payload' = sizedN 38 payload
282 in case B.unpack xs of
283 [isResponse] | 0 <- isResponse -> UpToN INVITE_GROUPCHAT (B.concat [xs, (B.take 2 ys), sizedN 33 newid]) -- keep group number
284 [isResponse] | 1 <- isResponse -> UpToN INVITE_GROUPCHAT (B.concat [xs, (B.take 4 ys), sizedN 33 newid]) -- keep two group numbers
285 _ -> msg -- unexpected condition, leave unchanged
286
287 setGroupChatID (UpToN ONLINE_PACKET payload) (GrpId newid) = UpToN ONLINE_PACKET (B.concat [B.take 2 payload, sizedN 33 newid])
288 setGroupChatID _ _= error "setGroupChatID on non-groupchat message."
289
290groupChatID :: (Functor f, HasGroupChatID x) => (GroupChatId -> f GroupChatId) -> (x -> f x)
291groupChatID = lens getGroupChatID setGroupChatID
292
293type GroupNumber = Word16
294type PeerNumber = Word16
295type MessageNumber = Word32
296
297class HasGroupNumber x where
298 getGroupNumber :: x -> GroupNumber
299 setGroupNumber :: x -> GroupNumber -> x
300
301instance HasGroupNumber CryptoMessage where
302 getGroupNumber (UpToN INVITE_GROUPCHAT (sizedN 39 -> B.uncons -> Just (isResp,xs))) -- note isResp should be 0 or 1
303 = let twobytes = B.take 2 xs
304 Right n = S.decode twobytes
305 in n
306 getGroupNumber (UpToN (fromEnum -> x) (sizedN 2 -> twobytes)) | x >= 0x61 && x <= 0x63
307 = let Right n = S.decode twobytes in n
308 getGroupNumber (UpToN (fromEnum -> 0xC7) (sizedN 2 -> twobytes))
309 = let Right n = S.decode twobytes in n
310
311 getGroupNumber _ = error "getGroupNumber on CryptoMessage without group number field."
312
313 setGroupNumber (UpToN INVITE_GROUPCHAT (sizedN 39 -> B.uncons -> Just (isResp,xs))) groupnum
314 = UpToN INVITE_GROUPCHAT (B.cons isResp (B.append (S.encode groupnum) (B.drop 2 xs)))
315 setGroupNumber (UpToN xE@(fromEnum -> x) (sizedAtLeastN 2 -> B.splitAt 2 -> (twobytes,xs))) groupnum
316 | x >= 0x61 && x <= 0x63 = UpToN xE (B.append (S.encode groupnum) xs)
317 | x == 0xC7 = UpToN xE (B.append (S.encode groupnum) xs)
318 setGroupNumber _ _ = error "setGroupNumber on CryptoMessage without group number field."
319
320groupNumber :: (Functor f, HasGroupNumber x) => (Word16 -> f Word16) -> (x -> f x)
321groupNumber = lens getGroupNumber setGroupNumber
322
323class HasGroupNumberToJoin x where
324 getGroupNumberToJoin :: x -> GroupNumber
325 setGroupNumberToJoin :: x -> GroupNumber -> x
326
327instance HasGroupNumberToJoin CryptoMessage where
328 getGroupNumberToJoin (UpToN INVITE_GROUPCHAT (sizedN 39 -> B.uncons -> Just (1,xs))) -- only response has to-join
329 = let twobytes = B.take 2 (B.drop 2 xs) -- skip group number (local)
330 Right n = S.decode twobytes
331 in n
332 getGroupNumberToJoin _ = error "getGroupNumberToJoin on CryptoMessage without group number (to join) field."
333 setGroupNumberToJoin (UpToN INVITE_GROUPCHAT (sizedN 39 -> B.uncons -> Just (1,xs))) groupnum
334 = let (a,b) = B.splitAt 2 xs
335 (twoBytes,c) = B.splitAt 2 b
336 twoBytes' = S.encode groupnum
337 in UpToN INVITE_GROUPCHAT (B.cons 1 (B.concat [a,twoBytes',c]))
338 setGroupNumberToJoin _ _ = error "setGroupNumberToJoin on CryptoMessage without group number (to join) field."
339
340groupNumberToJoin :: (Functor f, HasGroupNumberToJoin x) => (GroupNumber -> f GroupNumber) -> (x -> f x)
341groupNumberToJoin = lens getGroupNumberToJoin setGroupNumberToJoin
342
343class HasPeerNumber x where
344 getPeerNumber :: x -> PeerNumber
345 setPeerNumber :: x -> PeerNumber -> x
346
347instance HasPeerNumber CryptoMessage where
348 getPeerNumber (UpToN (fromEnum -> 0x63) (sizedN 4 -> B.splitAt 2 -> (grpnum,twobytes)))
349 = let Right n = S.decode twobytes in n
350 getPeerNumber (UpToN (fromEnum -> 0xC7) (sizedN 4 -> B.splitAt 2 -> (grpnum,twobytes)))
351 = let Right n = S.decode twobytes in n
352 getPeerNumber _ = error "getPeerNumber on CryptoMessage without peer number field."
353
354 setPeerNumber (UpToN xE@(fromEnum -> 0x63) (sizedAtLeastN 4 -> B.splitAt 2 -> (gnum,xs))) peernum
355 = UpToN xE (B.concat [gnum,S.encode peernum, B.drop 2 xs])
356 setPeerNumber (UpToN xE@(fromEnum -> 0xC7) (sizedAtLeastN 4 -> B.splitAt 2 -> (gnum,xs))) peernum
357 = UpToN xE (B.concat [gnum,S.encode peernum, B.drop 2 xs])
358 setPeerNumber _ _ = error "setPeerNumber on CryptoMessage without peer number field."
359
360peerNumber :: (Functor f, HasPeerNumber x) => (Word16 -> f Word16) -> (x -> f x)
361peerNumber = lens getPeerNumber setPeerNumber
362
363class HasMessageNumber x where
364 getMessageNumber :: x -> MessageNumber
365 setMessageNumber :: x -> MessageNumber -> x
366
367instance HasMessageNumber CryptoMessage where
368 getMessageNumber (UpToN (fromEnum -> 0x63) (sizedN 8 -> B.splitAt 4 -> (_,fourbytes)))
369 = let Right n = S.decode fourbytes in n
370 getMessageNumber (UpToN (fromEnum -> 0xC7) (sizedN 8 -> B.splitAt 4 -> (_,fourbytes)))
371 = let Right n = S.decode fourbytes in n
372 getMessageNumber _ = error "getMessageNumber on CryptoMessage without message number field."
373
374 setMessageNumber (UpToN xE@(fromEnum -> 0x63) (sizedAtLeastN 8 -> B.splitAt 4 -> (bs,xs))) messagenum
375 = UpToN xE (B.concat [bs,S.encode messagenum, B.drop 4 xs])
376 setMessageNumber (UpToN xE@(fromEnum -> 0xC7) (sizedAtLeastN 8 -> B.splitAt 4 -> (bs,xs))) messagenum
377 = UpToN xE (B.concat [bs,S.encode messagenum, B.drop 4 xs])
378 setMessageNumber _ _ = error "setMessageNumber on CryptoMessage without message number field."
379
380messageNumber :: (Functor f, HasMessageNumber x) => (Word32 -> f Word32) -> (x -> f x)
381messageNumber = lens getMessageNumber setMessageNumber
382
383
384class HasMessageName x where
385 getMessageName :: x -> MessageName
386 setMessageName :: x -> MessageName -> x
387
388instance HasMessageName CryptoMessage where
389 getMessageName (UpToN (fromEnum -> 0x63) (sizedN 9 -> B.splitAt 8 -> (_,onebyte)))
390 = let [n] = B.unpack onebyte
391 in toEnum . fromIntegral $ n
392 getMessageName (UpToN (fromEnum -> 0xC7) (sizedN 9 -> B.splitAt 8 -> (_,onebyte)))
393 = let [n] = B.unpack onebyte
394 in toEnum . fromIntegral $ n
395 getMessageName _ = error "getMessageName on CryptoMessage without message name field."
396
397 setMessageName (UpToN xE@(fromEnum -> 0x63) (sizedAtLeastN 9 -> B.splitAt 8 -> (bs,xs))) messagename
398 = UpToN xE (B.concat [bs,B.cons (fromIntegral $ fromEnum messagename) (B.drop 1 xs)])
399 setMessageName (UpToN xE@(fromEnum -> 0xC7) (sizedAtLeastN 9 -> B.splitAt 8 -> (bs,xs))) messagename
400 = UpToN xE (B.concat [bs,B.cons (fromIntegral $ fromEnum messagename) (B.drop 1 xs)])
401 setMessageName _ _ = error "setMessageName on CryptoMessage without message name field."
402
403messageName :: (Functor f, HasMessageName x) => (MessageName -> f MessageName) -> (x -> f x)
404messageName = lens getMessageName setMessageName
405
406data MessageType = Msg MessageID
407 | GrpMsg MessageName
408 deriving (Eq,Show)
409
410instance Ord MessageType where
411 compare (Msg x) (Msg y) = compare x y
412 compare (GrpMsg x) (GrpMsg y) = compare x y
413 compare (Msg _) (GrpMsg _) = LT
414 compare (GrpMsg _) (Msg _) = GT
415
416class HasMessageType x where
417 getMessageType :: x -> MessageType
418 setMessageType :: x -> MessageType -> x
419
420instance HasMessageType CryptoMessage where
421 getMessageType (OneByte mid) = Msg mid
422 getMessageType (TwoByte mid _) = Msg mid
423 getMessageType m@(UpToN mid _) | isIndirectGrpChat mid = GrpMsg (getMessageName m)
424 getMessageType (UpToN mid _) = Msg mid
425
426 setMessageType m@(UpToN mid _) (GrpMsg mname) | isIndirectGrpChat mid = setMessageName m mname
427 setMessageType (OneByte _ ) (GrpMsg mname) = setMessageName (UpToN MESSAGE_GROUPCHAT B.empty ) mname
428 setMessageType (TwoByte _ x) (GrpMsg mname) = setMessageName (UpToN MESSAGE_GROUPCHAT (B.singleton x)) mname
429 setMessageType (UpToN _ x) (GrpMsg mname) = setMessageName (UpToN MESSAGE_GROUPCHAT x) mname
430 setMessageType m (Msg mid) | Just (True,1) <- msgSizeParam mid = OneByte mid
431 setMessageType (OneByte mid0 ) (Msg mid) | Just (True,2) <- msgSizeParam mid = TwoByte mid 0
432 setMessageType (TwoByte mid0 x) (Msg mid) | Just (True,2) <- msgSizeParam mid = TwoByte mid x
433 setMessageType (UpToN mid0 x) (Msg mid) | Just (True,n) <- msgSizeParam mid = UpToN mid (sizedN n x)
434 setMessageType (OneByte mid0) (Msg mid) = UpToN mid B.empty
435 setMessageType (TwoByte mid0 x) (Msg mid) = UpToN mid (B.singleton x)
436 setMessageType (UpToN mid0 x) (Msg mid) = UpToN mid x
437
438instance HasMessageType CryptoData where
439 getMessageType (CryptoData { bufferData }) = getMessageType bufferData
440 setMessageType cd@(CryptoData { bufferData=bd }) typ = cd { bufferData=setMessageType bd typ }
441
442-- | This lens should always succeed on CryptoMessage
443messageType :: (Functor f, HasMessageType x) => (MessageType -> f MessageType) -> (x -> f x)
444messageType = lens getMessageType setMessageType
445
446type MessageData = B.ByteString
447
448class HasMessageData x where
449 getMessageData :: x -> MessageData
450 setMessageData :: x -> MessageData -> x
451
452instance HasMessageData CryptoMessage where
453 getMessageData (UpToN (fromEnum -> 0x63) (sizedAtLeastN 9 -> B.splitAt 9 -> (_,mdata))) = mdata
454 getMessageData (UpToN (fromEnum -> 0xC7) (sizedAtLeastN 9 -> B.splitAt 9 -> (_,mdata))) = mdata
455 getMessageData (UpToN (fromEnum -> 0x62) (sizedAtLeastN 3 -> B.splitAt 2 -> (_,B.uncons -> Just (0x09,peerinfos)))) = peerinfos
456 -- getMessageData on 0x62:0a is equivalent to getTitle but without decoding the utf8
457 getMessageData (UpToN (fromEnum -> 0x62) (sizedAtLeastN 3 -> B.splitAt 2 -> (_,B.uncons -> Just (0x0a,title)))) = title
458 getMessageData _ = error "getMessageData on CryptoMessage without message data field."
459
460 setMessageData (UpToN xE@(fromEnum -> 0x63) (sizedAtLeastN 9 -> B.splitAt 9 -> (bs,xs))) messagedata -- MESSAGE_GROUPCHAT
461 = UpToN xE (B.concat [bs,messagedata])
462 setMessageData (UpToN xE@(fromEnum -> 0xC7) (sizedAtLeastN 9 -> B.splitAt 9 -> (bs,xs))) messagedata -- LOSSY_GROUPCHAT
463 = UpToN xE (B.concat [bs,messagedata])
464 setMessageData (UpToN xE@(fromEnum -> 0x62) (sizedAtLeastN 3 -> B.splitAt 3 -> (bs,xs))) peerinfosOrTitle -- peer/title response packets
465 = UpToN xE (B.concat [bs,peerinfosOrTitle])
466 setMessageData _ _ = error "setMessageData on CryptoMessage without message data field."
467
468messageData :: (Functor f, HasMessageData x) => (MessageData -> f MessageData) -> (x -> f x)
469messageData = lens getMessageData setMessageData
470
471class HasTitle x where
472 getTitle :: x -> Text
473 setTitle :: x -> Text -> x
474
475instance HasTitle CryptoMessage where
476 getTitle (UpToN DIRECT_GROUPCHAT {-Ox62-} (sizedAtLeastN 3 -> B.splitAt 2 -> (_,B.uncons -> Just (0x0a,mdata)))) = decodeUtf8 mdata
477 getTitle (UpToN xE (sizedAtLeastN 9 -> B.splitAt 8 -> (_,B.uncons -> Just (toEnum . fromIntegral -> GroupchatTitleChange,mdata))))
478 | isIndirectGrpChat xE = decodeUtf8 mdata
479 getTitle _ = error "getTitle on CryptoMessage without title field."
480
481 setTitle (UpToN xE@DIRECT_GROUPCHAT {-0x62-} (sizedAtLeastN 3 -> B.splitAt 2 -> (bs,B.uncons -> Just (_,xs)))) messagedata
482 = UpToN xE (B.concat [bs,B.cons 0x0a (encodeUtf8 messagedata)])
483 setTitle (UpToN xE (sizedAtLeastN 9 -> B.splitAt 8 -> (bs,B.uncons -> Just (_,xs)))) title
484 | isIndirectGrpChat xE = UpToN xE (B.concat [bs,B.cons (fromIntegral $ fromEnum GroupchatTitleChange) (encodeUtf8 title)])
485 setTitle _ _ = error "setTitle on CryptoMessage without title field."
486
487title :: (Functor f, HasTitle x) => (Text -> f Text) -> (x -> f x)
488title = lens getTitle setTitle
489
490class HasMessage x where
491 getMessage :: x -> Text
492 setMessage :: x -> Text -> x
493
494instance HasMessage CryptoMessage where
495 getMessage (UpToN MESSAGE bstr) = T.decodeUtf8 bstr
496 getMessage (UpToN xE (sizedAtLeastN 9 -> B.splitAt 8 -> (_,B.uncons -> Just (mnameByte,mdata))))
497 | isIndirectGrpChat xE = decodeUtf8 mdata
498 getMessage _ = error "getMessage on CryptoMessage without message field."
499
500 setMessage (UpToN MESSAGE _) message = UpToN MESSAGE (T.encodeUtf8 $ message)
501 setMessage (UpToN xE (sizedAtLeastN 9 -> B.splitAt 8 -> (bs,B.uncons -> Just (mnameByte,xs)))) message
502 | isIndirectGrpChat xE = UpToN xE (B.concat [bs,B.cons (if mnameByte == 0 then 0x40 else mnameByte) (encodeUtf8 message)])
503 setMessage _ _ = error "setMessage on CryptoMessage without message field."
504
505message :: (Functor f, HasMessage x) => (Text -> f Text) -> (x -> f x)
506message = lens getMessage setMessage
507
508class HasName x where
509 getName :: x -> Text
510 setName :: x -> Text -> x
511
512
513instance HasName CryptoMessage where
514 -- Only MESSAGE_GROUPCHAT:NameChange has Name field
515 getName (UpToN xE (sizedAtLeastN 4 -> B.splitAt 3 -> (_,B.uncons -> Just (toEnum . fromIntegral -> NameChange,mdata)))) | isIndirectGrpChat xE = decodeUtf8 mdata
516 getName _ = error "getName on CryptoMessage without name field."
517
518 -- If its not NameChange, this setter will set it to NameChange
519 setName (UpToN xE (sizedAtLeastN 4 -> B.splitAt 3 -> (bs,B.uncons -> Just (_,xs)))) name
520 | isIndirectGrpChat xE = UpToN xE (B.concat [bs,B.cons (fromIntegral $ fromEnum NameChange) (encodeUtf8 name)])
521 setName _ _ = error "setName on CryptoMessage without name field."
522
523name :: (Functor f, HasTitle x) => (Text -> f Text) -> (x -> f x)
524name = lens getTitle setTitle
525
526data PeerInfo
527 = PeerInfo
528 { piPeerNum :: PeerNumber
529 , piUserKey :: PublicKey
530 , piDHTKey :: PublicKey
531 , piName :: ByteString -- byte-prefix for length
532 } deriving (Eq,Show)
533
534instance HasPeerNumber PeerInfo where
535 getPeerNumber = piPeerNum
536 setPeerNumber x n = x { piPeerNum = n }
537
538instance Serialize PeerInfo where
539 get = do
540 w16 <- get
541 ukey <- id2key <$> get
542 dkey <- id2key <$> get
543 w8 <- get :: Get Word8
544 PeerInfo w16 ukey dkey <$> getBytes (fromIntegral w8)
545
546 put (PeerInfo w16 ukey dkey bs) = do
547 put w16
548 put $ key2id ukey
549 put $ key2id dkey
550 let sz :: Word8
551 sz = case B.length bs of
552 n | n <= 255 -> fromIntegral n
553 | otherwise -> 255
554 put sz
555 putByteString $ B.take (fromIntegral sz) bs
556
557
558-- |
559-- default constructor, handy for formations such as:
560--
561-- > userStatus .~ Busy $ msg USERSTATUS
562--
563msg :: MessageID -> CryptoMessage
564msg mid | Just (True,0) <- msgSizeParam mid = OneByte mid
565msg mid | Just (True,1) <- msgSizeParam mid = TwoByte mid 0
566msg mid | Just (False,_) <- msgSizeParam mid = UpToN mid B.empty
567msg mid = UpToN mid B.empty
568
569leaveMsg :: Serialize a => a -> CryptoMessage
570leaveMsg groupnum = UpToN DIRECT_GROUPCHAT (B.snoc (S.encode groupnum) 0x01)
571
572peerQueryMsg :: Serialize a => a -> CryptoMessage
573peerQueryMsg groupnum = UpToN DIRECT_GROUPCHAT (B.snoc (S.encode groupnum) 0x08)
574
575
576-- | Returns if the given message is of fixed(OneByte/TwoByte) size, as well as
577-- the maximum allowed size for the message Payload (message minus id)
578-- Or Nothing if unknown/unimplemented.
579msgSizeParam :: MessageID -> Maybe (Bool,Int)
580msgSizeParam ONLINE = Just (True,0)
581msgSizeParam OFFLINE = Just (True,0)
582msgSizeParam USERSTATUS = Just (True,1)
583msgSizeParam TYPING = Just (True,1)
584msgSizeParam NICKNAME = Just (False,128)
585msgSizeParam STATUSMESSAGE = Just (False,1007)
586msgSizeParam MESSAGE = Just (False,1372)
587msgSizeParam ACTION = Just (False,1372)
588msgSizeParam FILE_DATA = Just (False,1372)-- up to 1373
589msgSizeParam FILE_SENDREQUEST = Just (False,300) -- 1+1+4+8+32+max255 = up to 301
590msgSizeParam FILE_CONTROL = Just (False,7) -- 8 bytes if seek, otherwise 4
591msgSizeParam INVITE_GROUPCHAT = Just (False,38)
592msgSizeParam ONLINE_PACKET = Just (True,35)
593msgSizeParam DIRECT_GROUPCHAT {-0x62-} = Nothing -- 1+2+1 thus Just (True,3) leave & peer-query, but variable in response packets
594msgSizeParam MESSAGE_GROUPCHAT {-0x63-} = Nothing -- variable
595msgSizeParam LOSSY_GROUPCHAT {-0xC7-} = Nothing -- variable
596msgSizeParam _ = Nothing
597
598isIndirectGrpChat :: MessageID -> Bool
599isIndirectGrpChat MESSAGE_GROUPCHAT = True
600isIndirectGrpChat LOSSY_GROUPCHAT = True
601isIndirectGrpChat _ = False
602
603data LossyOrLossless = UnknownLossyness | Lossless | Lossy
604 deriving (Eq,Ord,Enum,Show,Bounded)
605
606lossyness :: MessageID -> LossyOrLossless
607lossyness (fromEnum -> x) | x < 3 = Lossy
608lossyness (fromEnum -> x) | x >= 16, x < 192 = Lossless
609lossyness (fromEnum -> x) | x >= 192, x < 255 = Lossy
610lossyness (fromEnum -> 255) = Lossless
611lossyness _ = UnknownLossyness
612
613-- TODO: Flesh this out.
614data MessageID -- First byte indicates data
615 = Padding -- ^ 0 padding (skipped until we hit a non zero (data id) byte)
616 | PacketRequest -- ^ 1 packet request packet (lossy packet)
617 | KillPacket -- ^ 2 connection kill packet (lossy packet)
618 | UnspecifiedPacket003 -- ^ 3+ unspecified
619 | UnspecifiedPacket004
620 | UnspecifiedPacket005
621 | UnspecifiedPacket006
622 | UnspecifiedPacket007
623 | UnspecifiedPacket008
624 | UnspecifiedPacket009
625 | UnspecifiedPacket010
626 | UnspecifiedPacket011
627 | UnspecifiedPacket012
628 | UnspecifiedPacket013
629 | UnspecifiedPacket014
630 | UnspecifiedPacket015
631 | MessengerLossless016 -- ^ 16+ reserved for Messenger usage (lossless packets)
632 | MessengerLossless017
633 | MessengerLossless018
634 | MessengerLossless019
635 | MessengerLossless020
636 | MessengerLossless021
637 | MessengerLossless022
638 | MessengerLossless023
639 | ONLINE -- 1 byte
640 | OFFLINE -- 1 byte
641 | MessengerLossless026
642 | MessengerLossless027
643 | MessengerLossless028
644 | MessengerLossless029
645 | MessengerLossless030
646 | MessengerLossless031
647 | MessengerLossless032
648 | MessengerLossless033
649 | MessengerLossless034
650 | MessengerLossless035
651 | MessengerLossless036
652 | MessengerLossless037
653 | MessengerLossless038
654 | MessengerLossless039
655 | MessengerLossless040
656 | MessengerLossless041
657 | MessengerLossless042
658 | MessengerLossless043
659 | MessengerLossless044
660 | MessengerLossless045
661 | MessengerLossless046
662 | MessengerLossless047
663 | NICKNAME -- up to 129 bytes
664 | STATUSMESSAGE -- up to 1008 bytes
665 | USERSTATUS -- 2 bytes
666 | TYPING -- 2 bytes
667 | MessengerLossless052
668 | MessengerLossless053
669 | MessengerLossless054
670 | MessengerLossless055
671 | MessengerLossless056
672 | MessengerLossless057
673 | MessengerLossless058
674 | MessengerLossless059
675 | MessengerLossless060
676 | MessengerLossless061
677 | MessengerLossless062
678 | MessengerLossless063
679 | MESSAGE -- up to 1373 bytes
680 | ACTION -- up to 1373 bytes
681 | MessengerLossless066
682 | MessengerLossless067
683 | MessengerLossless068
684 | MSI
685 | MessengerLossless070
686 | MessengerLossless071
687 | MessengerLossless072
688 | MessengerLossless073
689 | MessengerLossless074
690 | MessengerLossless075
691 | MessengerLossless076
692 | MessengerLossless077
693 | MessengerLossless078
694 | MessengerLossless079
695 | FILE_SENDREQUEST -- 1+1+4+8+32+max255 = up to 301
696 | FILE_CONTROL -- 8 bytes if seek, otherwise 4
697 | FILE_DATA -- up to 1373
698 | MessengerLossless083
699 | MessengerLossless084
700 | MessengerLossless085
701 | MessengerLossless086
702 | MessengerLossless087
703 | MessengerLossless088
704 | MessengerLossless089
705 | MessengerLossless090
706 | MessengerLossless091
707 | MessengerLossless092
708 | MessengerLossless093
709 | MessengerLossless094
710 | MessengerLossless095
711 | INVITE_GROUPCHAT -- 0x60
712 | ONLINE_PACKET -- 0x61
713 | DIRECT_GROUPCHAT -- 0x62
714 | MESSAGE_GROUPCHAT -- 0x63
715 | MessengerLossless100
716 | MessengerLossless101
717 | MessengerLossless102
718 | MessengerLossless103
719 | MessengerLossless104
720 | MessengerLossless105
721 | MessengerLossless106
722 | MessengerLossless107
723 | MessengerLossless108
724 | MessengerLossless109
725 | MessengerLossless110
726 | MessengerLossless111
727 | MessengerLossless112
728 | MessengerLossless113
729 | MessengerLossless114
730 | MessengerLossless115
731 | MessengerLossless116
732 | MessengerLossless117
733 | MessengerLossless118
734 | MessengerLossless119
735 | MessengerLossless120
736 | MessengerLossless121
737 | MessengerLossless122
738 | MessengerLossless123
739 | MessengerLossless124
740 | MessengerLossless125
741 | MessengerLossless126
742 | MessengerLossless127
743 | MessengerLossless128
744 | MessengerLossless129
745 | MessengerLossless130
746 | MessengerLossless131
747 | MessengerLossless132
748 | MessengerLossless133
749 | MessengerLossless134
750 | MessengerLossless135
751 | MessengerLossless136
752 | MessengerLossless137
753 | MessengerLossless138
754 | MessengerLossless139
755 | MessengerLossless140
756 | MessengerLossless141
757 | MessengerLossless142
758 | MessengerLossless143
759 | MessengerLossless144
760 | MessengerLossless145
761 | MessengerLossless146
762 | MessengerLossless147
763 | MessengerLossless148
764 | MessengerLossless149
765 | MessengerLossless150
766 | MessengerLossless151
767 | MessengerLossless152
768 | MessengerLossless153
769 | MessengerLossless154
770 | MessengerLossless155
771 | MessengerLossless156
772 | MessengerLossless157
773 | MessengerLossless158
774 | MessengerLossless159
775 | MessengerLossless160
776 | MessengerLossless161
777 | MessengerLossless162
778 | MessengerLossless163
779 | MessengerLossless164
780 | MessengerLossless165
781 | MessengerLossless166
782 | MessengerLossless167
783 | MessengerLossless168
784 | MessengerLossless169
785 | MessengerLossless170
786 | MessengerLossless171
787 | MessengerLossless172
788 | MessengerLossless173
789 | MessengerLossless174
790 | MessengerLossless175
791 | MessengerLossless176
792 | MessengerLossless177
793 | MessengerLossless178
794 | MessengerLossless179
795 | MessengerLossless180
796 | MessengerLossless181
797 | MessengerLossless182
798 | MessengerLossless183
799 | MessengerLossless184
800 | MessengerLossless185
801 | MessengerLossless186
802 | MessengerLossless187
803 | MessengerLossless188
804 | MessengerLossless189
805 | MessengerLossless190
806 | MessengerLossless191
807 | MessengerLossy192 -- ^ 192+ reserved for Messenger usage (lossy packets)
808 | MessengerLossy193
809 | MessengerLossy194
810 | MessengerLossy195
811 | MessengerLossy196
812 | MessengerLossy197
813 | MessengerLossy198
814 | LOSSY_GROUPCHAT -- 0xC7
815 | MessengerLossy200
816 | MessengerLossy201
817 | MessengerLossy202
818 | MessengerLossy203
819 | MessengerLossy204
820 | MessengerLossy205
821 | MessengerLossy206
822 | MessengerLossy207
823 | MessengerLossy208
824 | MessengerLossy209
825 | MessengerLossy210
826 | MessengerLossy211
827 | MessengerLossy212
828 | MessengerLossy213
829 | MessengerLossy214
830 | MessengerLossy215
831 | MessengerLossy216
832 | MessengerLossy217
833 | MessengerLossy218
834 | MessengerLossy219
835 | MessengerLossy220
836 | MessengerLossy221
837 | MessengerLossy222
838 | MessengerLossy223
839 | MessengerLossy224
840 | MessengerLossy225
841 | MessengerLossy226
842 | MessengerLossy227
843 | MessengerLossy228
844 | MessengerLossy229
845 | MessengerLossy230
846 | MessengerLossy231
847 | MessengerLossy232
848 | MessengerLossy233
849 | MessengerLossy234
850 | MessengerLossy235
851 | MessengerLossy236
852 | MessengerLossy237
853 | MessengerLossy238
854 | MessengerLossy239
855 | MessengerLossy240
856 | MessengerLossy241
857 | MessengerLossy242
858 | MessengerLossy243
859 | MessengerLossy244
860 | MessengerLossy245
861 | MessengerLossy246
862 | MessengerLossy247
863 | MessengerLossy248
864 | MessengerLossy249
865 | MessengerLossy250
866 | MessengerLossy251
867 | MessengerLossy252
868 | MessengerLossy253
869 | MessengerLossy254
870 | Messenger255 -- ^ 255 reserved for Messenger usage (lossless packet)
871 deriving (Show,Eq,Enum,Ord,Bounded)
872
873
874
875data MessageName = Ping -- 0x00
876 | MessageName0x01
877 | MessageName0x02
878 | MessageName0x03
879 | MessageName0x04
880 | MessageName0x05
881 | MessageName0x06
882 | MessageName0x07
883 | MessageName0x08
884 | MessageName0x09
885 | MessageName0x0a
886 | MessageName0x0b
887 | MessageName0x0c
888 | MessageName0x0d
889 | MessageName0x0e
890 | MessageName0x0f
891 | NewPeer -- 0x10
892 | KillPeer -- 0x11
893 | MessageName0x12
894 | MessageName0x13
895 | MessageName0x14
896 | MessageName0x15
897 | MessageName0x16
898 | MessageName0x17
899 | MessageName0x18
900 | MessageName0x19
901 | MessageName0x1a
902 | MessageName0x1b
903 | MessageName0x1c
904 | MessageName0x1d
905 | MessageName0x1e
906 | MessageName0x1f
907 | MessageName0x20
908 | MessageName0x21
909 | MessageName0x22
910 | MessageName0x23
911 | MessageName0x24
912 | MessageName0x25
913 | MessageName0x26
914 | MessageName0x27
915 | MessageName0x28
916 | MessageName0x29
917 | MessageName0x2a
918 | MessageName0x2b
919 | MessageName0x2c
920 | MessageName0x2d
921 | MessageName0x2e
922 | MessageName0x2f
923 | NameChange -- 0x30
924 | GroupchatTitleChange -- 0x31
925 | MessageName0x32
926 | MessageName0x33
927 | MessageName0x34
928 | MessageName0x35
929 | MessageName0x36
930 | MessageName0x37
931 | MessageName0x38
932 | MessageName0x39
933 | MessageName0x3a
934 | MessageName0x3b
935 | MessageName0x3c
936 | MessageName0x3d
937 | MessageName0x3e
938 | MessageName0x3f
939 | ChatMessage -- 0x40
940 | Action -- 0x41
941 | MessageName0x42
942 | MessageName0x43
943 | MessageName0x44
944 | MessageName0x45
945 | MessageName0x46
946 | MessageName0x47
947 | MessageName0x48
948 | MessageName0x49
949 | MessageName0x4a
950 | MessageName0x4b
951 | MessageName0x4c
952 | MessageName0x4d
953 | MessageName0x4e
954 | MessageName0x4f
955 | MessageName0x50
956 | MessageName0x51
957 | MessageName0x52
958 | MessageName0x53
959 | MessageName0x54
960 | MessageName0x55
961 | MessageName0x56
962 | MessageName0x57
963 | MessageName0x58
964 | MessageName0x59
965 | MessageName0x5a
966 | MessageName0x5b
967 | MessageName0x5c
968 | MessageName0x5d
969 | MessageName0x5e
970 | MessageName0x5f
971 | MessageName0x60
972 | MessageName0x61
973 | MessageName0x62
974 | MessageName0x63
975 | MessageName0x64
976 | MessageName0x65
977 | MessageName0x66
978 | MessageName0x67
979 | MessageName0x68
980 | MessageName0x69
981 | MessageName0x6a
982 | MessageName0x6b
983 | MessageName0x6c
984 | MessageName0x6d
985 | MessageName0x6e
986 | MessageName0x6f
987 | MessageName0x70
988 | MessageName0x71
989 | MessageName0x72
990 | MessageName0x73
991 | MessageName0x74
992 | MessageName0x75
993 | MessageName0x76
994 | MessageName0x77
995 | MessageName0x78
996 | MessageName0x79
997 | MessageName0x7a
998 | MessageName0x7b
999 | MessageName0x7c
1000 | MessageName0x7d
1001 | MessageName0x7e
1002 | MessageName0x7f
1003 | MessageName0x80
1004 | MessageName0x81
1005 | MessageName0x82
1006 | MessageName0x83
1007 | MessageName0x84
1008 | MessageName0x85
1009 | MessageName0x86
1010 | MessageName0x87
1011 | MessageName0x88
1012 | MessageName0x89
1013 | MessageName0x8a
1014 | MessageName0x8b
1015 | MessageName0x8c
1016 | MessageName0x8d
1017 | MessageName0x8e
1018 | MessageName0x8f
1019 | MessageName0x90
1020 | MessageName0x91
1021 | MessageName0x92
1022 | MessageName0x93
1023 | MessageName0x94
1024 | MessageName0x95
1025 | MessageName0x96
1026 | MessageName0x97
1027 | MessageName0x98
1028 | MessageName0x99
1029 | MessageName0x9a
1030 | MessageName0x9b
1031 | MessageName0x9c
1032 | MessageName0x9d
1033 | MessageName0x9e
1034 | MessageName0x9f
1035 | MessageName0xa0
1036 | MessageName0xa1
1037 | MessageName0xa2
1038 | MessageName0xa3
1039 | MessageName0xa4
1040 | MessageName0xa5
1041 | MessageName0xa6
1042 | MessageName0xa7
1043 | MessageName0xa8
1044 | MessageName0xa9
1045 | MessageName0xaa
1046 | MessageName0xab
1047 | MessageName0xac
1048 | MessageName0xad
1049 | MessageName0xae
1050 | MessageName0xaf
1051 | MessageName0xb0
1052 | MessageName0xb1
1053 | MessageName0xb2
1054 | MessageName0xb3
1055 | MessageName0xb4
1056 | MessageName0xb5
1057 | MessageName0xb6
1058 | MessageName0xb7
1059 | MessageName0xb8
1060 | MessageName0xb9
1061 | MessageName0xba
1062 | MessageName0xbb
1063 | MessageName0xbc
1064 | MessageName0xbd
1065 | MessageName0xbe
1066 | MessageName0xbf
1067 | MessageName0xc0
1068 | MessageName0xc1
1069 | MessageName0xc2
1070 | MessageName0xc3
1071 | MessageName0xc4
1072 | MessageName0xc5
1073 | MessageName0xc6
1074 | MessageName0xc7
1075 | MessageName0xc8
1076 | MessageName0xc9
1077 | MessageName0xca
1078 | MessageName0xcb
1079 | MessageName0xcc
1080 | MessageName0xcd
1081 | MessageName0xce
1082 | MessageName0xcf
1083 | MessageName0xd0
1084 | MessageName0xd1
1085 | MessageName0xd2
1086 | MessageName0xd3
1087 | MessageName0xd4
1088 | MessageName0xd5
1089 | MessageName0xd6
1090 | MessageName0xd7
1091 | MessageName0xd8
1092 | MessageName0xd9
1093 | MessageName0xda
1094 | MessageName0xdb
1095 | MessageName0xdc
1096 | MessageName0xdd
1097 | MessageName0xde
1098 | MessageName0xdf
1099 | MessageName0xe0
1100 | MessageName0xe1
1101 | MessageName0xe2
1102 | MessageName0xe3
1103 | MessageName0xe4
1104 | MessageName0xe5
1105 | MessageName0xe6
1106 | MessageName0xe7
1107 | MessageName0xe8
1108 | MessageName0xe9
1109 | MessageName0xea
1110 | MessageName0xeb
1111 | MessageName0xec
1112 | MessageName0xed
1113 | MessageName0xee
1114 | MessageName0xef
1115 | MessageName0xf0
1116 | MessageName0xf1
1117 | MessageName0xf2
1118 | MessageName0xf3
1119 | MessageName0xf4
1120 | MessageName0xf5
1121 | MessageName0xf6
1122 | MessageName0xf7
1123 | MessageName0xf8
1124 | MessageName0xf9
1125 | MessageName0xfa
1126 | MessageName0xfb
1127 | MessageName0xfc
1128 | MessageName0xfd
1129 | MessageName0xfe
1130 | MessageName0xff
1131 deriving (Show,Eq,Ord,Enum,Bounded)
1132
1133-- --> CookieRequest WithoutCookie
1134-- <-- CookieResponse CookieAddress
1135-- --> Handshake CookieAddress
1136-- <-- Handshake CookieAddress
1137
1138-- cookie request packet (145 bytes)
1139--
1140-- [uint8_t 24]
1141-- [Sender's DHT Public key (32 bytes)]
1142-- [Random nonce (24 bytes)]
1143-- [Encrypted message containing:
1144-- [Sender's real public key (32 bytes)]
1145-- [padding (32 bytes)]
1146-- [uint64_t echo id (must be sent back untouched in cookie response)]
1147-- ]
1148
1149
1150-- cookie response packet (161 bytes):
1151--
1152-- [uint8_t 25]
1153-- [Random nonce (24 bytes)]
1154-- [Encrypted message containing:
1155-- [Cookie]
1156-- [uint64_t echo id (that was sent in the request)]
1157-- ]
1158--
1159-- Encrypted message is encrypted with the exact same symmetric key as the
1160-- cookie request packet it responds to but with a different nonce.
1161-- (Encrypted message is encrypted with reqesters's DHT private key,
1162-- responders's DHT public key and the nonce.)
1163--
1164-- Since we don't receive the public key, we will need to lookup the key by
1165-- the SockAddr... I don't understand why the CookieResponse message is
1166-- special this way. TODO: implement a multimap (SockAddr -> SharedSecret)
1167-- and wrap cookie queries with store/delete. TODO: Should the entire
1168-- SharedScret cache be keyed on only SockAddr ? Perhaps the secret cache
1169-- should be (NodeId -> Secret) and the cookie-request map should be
1170-- (SockAddr -> NodeId)
1171
1172
diff --git a/src/Network/Tox/DHT/Handlers.hs b/src/Network/Tox/DHT/Handlers.hs
new file mode 100644
index 00000000..500785cc
--- /dev/null
+++ b/src/Network/Tox/DHT/Handlers.hs
@@ -0,0 +1,432 @@
1{-# LANGUAGE CPP #-}
2{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3{-# LANGUAGE NamedFieldPuns #-}
4{-# LANGUAGE PatternSynonyms #-}
5{-# LANGUAGE TupleSections #-}
6module Network.Tox.DHT.Handlers where
7
8import Network.Tox.DHT.Transport as DHTTransport
9import Network.QueryResponse as QR hiding (Client)
10import qualified Network.QueryResponse as QR (Client)
11import Crypto.Tox
12import Network.Kademlia.Search
13import qualified Data.Wrapper.PSQInt as Int
14import Network.Kademlia
15import Network.Kademlia.Bootstrap
16import Network.Address (WantIP (..), ipFamily, testIdBit,fromSockAddr, sockAddrPort)
17import qualified Network.Kademlia.Routing as R
18import Control.TriadCommittee
19import System.Global6
20import OnionRouter
21
22import qualified Data.ByteArray as BA
23import qualified Data.ByteString.Char8 as C8
24import qualified Data.ByteString.Base16 as Base16
25import Control.Arrow
26import Control.Monad
27import Control.Concurrent.Lifted.Instrument
28import Control.Concurrent.STM
29import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
30import Network.Socket
31import Data.Hashable
32import Data.IP
33import Data.Ord
34import Data.Maybe
35import Data.Bits
36import Data.Serialize (Serialize)
37import Data.Word
38import Data.List
39import System.IO
40
41data TransactionId = TransactionId
42 { transactionKey :: Nonce8 -- ^ Used to lookup pending query.
43 , cryptoNonce :: Nonce24 -- ^ Used during the encryption layer.
44 }
45 deriving (Eq,Ord,Show)
46
47newtype PacketKind = PacketKind Word8
48 deriving (Eq, Ord, Serialize)
49
50pattern OnionRequest0Type = PacketKind 128 -- 0x80 Onion Request 0
51pattern OnionRequest1Type = PacketKind 129 -- 0x81 Onion Request 1
52pattern OnionRequest2Type = PacketKind 130 -- 0x82 Onion Request 2
53pattern AnnounceType = PacketKind 131 -- 0x83 Announce Request
54pattern AnnounceResponseType = PacketKind 132 -- 0x84 Announce Response
55
56pattern DataRequestType = PacketKind 133 -- 0x85 Onion Data Request (data to route request packet)
57pattern DataResponseType = PacketKind 134 -- 0x86 Onion Data Response (data to route response packet)
58-- 0x8c Onion Response 3
59-- 0x8d Onion Response 2
60pattern OnionResponse3Type = PacketKind 140 -- 0x8c Onion Response 3
61pattern OnionResponse2Type = PacketKind 141 -- 0x8d Onion Response 2
62pattern OnionResponse1Type = PacketKind 142 -- 0x8e Onion Response 1
63-- 0xf0 Bootstrap Info
64
65pattern DHTRequestType = PacketKind 32 -- 0x20 DHT Request
66
67pattern CookieRequestType = PacketKind 0x18
68pattern CookieResponseType = PacketKind 0x19
69
70pattern PingType = PacketKind 0 -- 0x00 Ping Request
71pattern PongType = PacketKind 1 -- 0x01 Ping Response
72pattern GetNodesType = PacketKind 2 -- 0x02 Nodes Request
73pattern SendNodesType = PacketKind 4 -- 0x04 Nodes Response
74
75
76instance Show PacketKind where
77 showsPrec d PingType = mappend "PingType"
78 showsPrec d PongType = mappend "PongType"
79 showsPrec d GetNodesType = mappend "GetNodesType"
80 showsPrec d SendNodesType = mappend "SendNodesType"
81 showsPrec d DHTRequestType = mappend "DHTRequestType"
82 showsPrec d OnionRequest0Type = mappend "OnionRequest0Type"
83 showsPrec d OnionResponse1Type = mappend "OnionResponse1Type"
84 showsPrec d OnionResponse3Type = mappend "OnionResponse3Type"
85 showsPrec d AnnounceType = mappend "AnnounceType"
86 showsPrec d AnnounceResponseType = mappend "AnnounceResponseType"
87 showsPrec d DataRequestType = mappend "DataRequestType"
88 showsPrec d DataResponseType = mappend "DataResponseType"
89 showsPrec d CookieRequestType = mappend "CookieRequestType"
90 showsPrec d CookieResponseType = mappend "CookieResponseType"
91 showsPrec d (PacketKind x) = mappend "PacketKind " . showsPrec (d+1) x
92
93msgType :: ( Serialize (f DHTRequest)
94 , Serialize (f Cookie), Serialize (f CookieRequest)
95 , Serialize (f SendNodes), Serialize (f GetNodes)
96 , Serialize (f Pong), Serialize (f Ping)
97 ) => DHTMessage f -> PacketKind
98msgType msg = PacketKind $ fst $ dhtMessageType msg
99
100classify :: Client -> Message -> MessageClass String PacketKind TransactionId NodeInfo Message
101classify client (DHTLanDiscovery {}) = IsUnsolicited (lanDiscoveryH client)
102classify client msg = fromMaybe (IsUnknown "unknown")
103 $ mapMessage (\nonce24 (nonce8,_) -> go msg (TransactionId nonce8 nonce24)) msg
104 where
105 go (DHTPing {}) = IsQuery PingType
106 go (DHTGetNodes {}) = IsQuery GetNodesType
107 go (DHTPong {}) = IsResponse
108 go (DHTSendNodes {}) = IsResponse
109 go (DHTCookieRequest {}) = IsQuery CookieRequestType
110 go (DHTCookie {}) = IsResponse
111 go (DHTDHTRequest {}) = IsQuery DHTRequestType
112
113data Routing = Routing
114 { tentativeId :: NodeInfo
115 , committee4 :: TriadCommittee NodeId SockAddr
116 , committee6 :: TriadCommittee NodeId SockAddr
117 , refresher4 :: BucketRefresher NodeId NodeInfo
118 , refresher6 :: BucketRefresher NodeId NodeInfo
119 }
120
121sched4 :: Routing -> TVar (Int.PSQ POSIXTime)
122sched4 Routing { refresher4 = BucketRefresher { refreshQueue } } = refreshQueue
123
124sched6 :: Routing -> TVar (Int.PSQ POSIXTime)
125sched6 Routing { refresher6 = BucketRefresher { refreshQueue } } = refreshQueue
126
127routing4 :: Routing -> TVar (R.BucketList NodeInfo)
128routing4 Routing { refresher4 = BucketRefresher { refreshBuckets } } = refreshBuckets
129
130routing6 :: Routing -> TVar (R.BucketList NodeInfo)
131routing6 Routing { refresher6 = BucketRefresher { refreshBuckets } } = refreshBuckets
132
133newRouting :: SockAddr -> TransportCrypto
134 -> (TVar (R.BucketList NodeInfo) -> SockAddr -> STM ()) -- ^ invoked on IPv4 change
135 -> (TVar (R.BucketList NodeInfo) -> SockAddr -> STM ()) -- ^ invoked on IPv6 change
136 -> IO (Client -> Routing)
137newRouting addr crypto update4 update6 = do
138 let tentative_ip4 = fromMaybe (IPv4 $ toEnum 0) (IPv4 <$> fromSockAddr addr)
139 tentative_ip6 = fromMaybe (IPv6 $ toEnum 0) (IPv6 <$> fromSockAddr addr)
140 tentative_info = NodeInfo
141 { nodeId = key2id $ transportPublic crypto
142 , nodeIP = fromMaybe (toEnum 0) (fromSockAddr addr)
143 , nodePort = fromMaybe 0 $ sockAddrPort addr
144 }
145 tentative_info4 = tentative_info { nodeIP = tentative_ip4 }
146 tentative_info6 <-
147 maybe (tentative_info { nodeIP = tentative_ip6 })
148 (\ip6 -> tentative_info { nodeIP = IPv6 ip6 })
149 <$> case addr of
150 SockAddrInet {} -> return Nothing
151 _ -> global6
152 atomically $ do
153 -- We defer initializing the refreshSearch and refreshPing until we
154 -- have a client to send queries with.
155 let nullPing = const $ return False
156 nullSearch = Search
157 { searchSpace = toxSpace
158 , searchNodeAddress = nodeIP &&& nodePort
159 , searchQuery = \_ _ -> return Nothing
160 }
161 refresher4 <- newBucketRefresher tentative_info nullSearch nullPing
162 refresher6 <- newBucketRefresher tentative_info6 nullSearch nullPing
163 let tbl4 = refreshBuckets refresher4
164 tbl6 = refreshBuckets refresher6
165 committee4 <- newTriadCommittee (update4 tbl4) -- updateIPVote tbl4 addr4
166 committee6 <- newTriadCommittee (update6 tbl6) -- updateIPVote tbl6 addr6
167 return $ \client ->
168 -- Now we have a client, so tell the BucketRefresher how to search and ping.
169 let updIO r = updateRefresherIO (nodeSearch client) (ping client) r
170 in Routing tentative_info committee4 committee6 (updIO refresher4) (updIO refresher6)
171
172
173-- TODO: This should cover more cases
174isLocal :: IP -> Bool
175isLocal (IPv6 ip6) = (ip6 == toEnum 0)
176isLocal (IPv4 ip4) = (ip4 == toEnum 0)
177
178isGlobal :: IP -> Bool
179isGlobal = not . isLocal
180
181prefer4or6 :: NodeInfo -> Maybe WantIP -> WantIP
182prefer4or6 addr iptyp = fromMaybe (ipFamily $ nodeIP addr) iptyp
183
184toxSpace :: R.KademliaSpace NodeId NodeInfo
185toxSpace = R.KademliaSpace
186 { R.kademliaLocation = nodeId
187 , R.kademliaTestBit = testNodeIdBit
188 , R.kademliaXor = xorNodeId
189 , R.kademliaSample = sampleNodeId
190 }
191
192
193pingH :: NodeInfo -> Ping -> IO Pong
194pingH _ Ping = return Pong
195
196getNodesH :: Routing -> NodeInfo -> GetNodes -> IO SendNodes
197getNodesH routing addr (GetNodes nid) = do
198 let preferred = prefer4or6 addr Nothing
199
200 (append4,append6) <- atomically $ do
201 ni4 <- R.thisNode <$> readTVar (routing4 routing)
202 ni6 <- R.thisNode <$> readTVar (routing6 routing)
203 return $ case ipFamily (nodeIP addr) of
204 Want_IP4 | isGlobal (nodeIP ni6) -> (id, (++ [ni6]))
205 Want_IP6 | isGlobal (nodeIP ni4) -> ((++ [ni4]), id)
206 _ -> (id, id)
207 ks <- go append4 $ routing4 routing
208 ks6 <- go append6 $ routing6 routing
209 let (ns1,ns2) = case preferred of Want_IP6 -> (ks6,ks)
210 Want_IP4 -> (ks,ks6)
211 Want_Both -> error $ "BUG:unreachable at " ++ __FILE__ ++ ":" ++ show __LINE__
212 return $ SendNodes
213 $ if null ns2 then ns1
214 else take 4 (take 3 ns1 ++ ns2)
215 where
216 go f var = f . R.kclosest toxSpace k nid <$> atomically (readTVar var)
217
218 k = 4
219
220cookieRequestH :: TransportCrypto -> NodeInfo -> CookieRequest -> IO Cookie
221cookieRequestH crypto ni (CookieRequest remoteUserKey) = do
222 hPutStrLn stderr $ "CookieRequest! remoteUserKey=" ++ show (key2id remoteUserKey)
223 (n24,sym,us) <- atomically $ do
224 n24 <- transportNewNonce crypto
225 sym <- transportSymmetric crypto
226 us <- readTVar $ userKeys crypto
227 return (n24,sym,us)
228 timestamp <- round . (* 1000000) <$> getPOSIXTime
229 let dta = encodePlain $ CookieData
230 { cookieTime = timestamp
231 , longTermKey = remoteUserKey
232 , dhtKey = transportPublic crypto
233 }
234 edta = encryptSymmetric sym n24 dta
235 hPutStrLn stderr $ "CookieRequest! responding to " ++ show (key2id remoteUserKey)
236 return $ Cookie n24 edta
237
238lanDiscoveryH :: Client -> NodeInfo -> NodeInfo -> IO (Maybe (Message -> Message))
239lanDiscoveryH client _ ni = do
240 forkIO $ do
241 myThreadId >>= flip labelThread "lan-discover-ping"
242 ping client ni
243 return ()
244 return Nothing
245
246type Message = DHTMessage ((,) Nonce8)
247
248type Client = QR.Client String PacketKind TransactionId NodeInfo Message
249
250
251wrapAsymm :: TransactionId -> NodeInfo -> NodeInfo -> (Nonce8 -> dta) -> Asymm dta
252wrapAsymm (TransactionId n8 n24) src dst dta = Asymm
253 { senderKey = id2key $ nodeId src
254 , asymmNonce = n24
255 , asymmData = dta n8
256 }
257
258serializer :: PacketKind
259 -> (Asymm (Nonce8,ping) -> Message)
260 -> (Message -> Maybe (Asymm (Nonce8,pong)))
261 -> MethodSerializer TransactionId NodeInfo Message PacketKind ping (Maybe pong)
262serializer pktkind mkping mkpong = MethodSerializer
263 { methodTimeout = \tid addr -> return (addr, 5000000)
264 , method = pktkind
265 -- wrapQuery :: tid -> addr -> addr -> qry -> x
266 , wrapQuery = \tid src dst ping -> mkping $ wrapAsymm tid src dst (, ping)
267 -- unwrapResponse :: x -> b
268 , unwrapResponse = fmap (snd . asymmData) . mkpong
269 }
270
271
272unpong :: Message -> Maybe (Asymm (Nonce8,Pong))
273unpong (DHTPong asymm) = Just asymm
274unpong _ = Nothing
275
276showHex :: BA.ByteArrayAccess ba => ba -> String
277showHex bs = C8.unpack $ Base16.encode $ BA.convert bs
278
279ping :: Client -> NodeInfo -> IO Bool
280ping client addr = do
281 hPutStrLn stderr $ show addr ++ " <-- ping"
282 reply <- QR.sendQuery client (serializer PingType DHTPing unpong) Ping addr
283 hPutStrLn stderr $ show addr ++ " -pong-> " ++ show reply
284 maybe (return False) (\Pong -> return True) $ join reply
285
286
287saveCookieKey :: TVar [(SockAddr, (Int, PublicKey))] -> SockAddr -> PublicKey -> STM ()
288saveCookieKey var saddr pk = do
289 cookiekeys <- readTVar var
290 case break (\(stored,_) -> stored == saddr) cookiekeys of
291 (xs,[]) -> writeTVar var $ (saddr, (1 ,pk)) : xs
292 (xs,(_,(c,stored)):ys) | stored == pk -> writeTVar var $ (saddr, (c+1,pk)) : xs ++ ys
293 _ -> retry -- Wait for requests to this address
294 -- under a different key to time out
295 -- before we try this key.
296
297loseCookieKey :: TVar [(SockAddr, (Int, PublicKey))] -> SockAddr -> PublicKey -> STM ()
298loseCookieKey var saddr pk = do
299 cookiekeys <- readTVar var
300 case break (\(stored,_) -> stored == saddr) cookiekeys of
301 (xs,(_,(1,stored)):ys) | stored == pk -> writeTVar var $ xs ++ ys
302 (xs,(_,(c,stored)):ys) | stored == pk -> writeTVar var $ (saddr, (c-1,pk)) : xs ++ ys
303 _ -> return () -- unreachable?
304
305
306cookieRequest :: TransportCrypto -> Client -> PublicKey -> NodeInfo -> IO (Maybe Cookie)
307cookieRequest crypto client localUserKey addr = do
308 let sockAddr = nodeAddr addr
309 nid = id2key $ nodeId addr
310 cookieSerializer
311 = MethodSerializer
312 { methodTimeout = \tid addr -> return (addr, 5000000)
313 , method = CookieRequestType
314 , wrapQuery = \tid src dst cr -> DHTCookieRequest $ wrapAsymm tid src dst (, cr)
315 , unwrapResponse = fmap snd . unCookie
316 }
317 cookieRequest = CookieRequest localUserKey
318 atomically $ saveCookieKey (pendingCookies crypto) sockAddr nid
319 hPutStrLn stderr $ show addr ++ " <-- cookieRequest"
320 reply <- QR.sendQuery client cookieSerializer cookieRequest addr
321 atomically $ loseCookieKey (pendingCookies crypto) sockAddr nid
322 hPutStrLn stderr $ show addr ++ " -cookieResponse-> " ++ show reply
323 return $ join reply
324
325unCookie :: DHTMessage t -> Maybe (t Cookie)
326unCookie (DHTCookie n24 fcookie) = Just fcookie
327unCookie _ = Nothing
328
329unsendNodes :: Message -> Maybe (Asymm (Nonce8,SendNodes))
330unsendNodes (DHTSendNodes asymm) = Just asymm
331unsendNodes _ = Nothing
332
333unwrapNodes :: SendNodes -> ( [NodeInfo], [NodeInfo], Maybe () )
334unwrapNodes (SendNodes ns) = (ns,ns,Just ())
335
336getNodes :: Client -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[NodeInfo],Maybe ()))
337getNodes client nid addr = do
338 -- hPutStrLn stderr $ show addr ++ " <-- getnodes " ++ show nid
339 reply <- QR.sendQuery client (serializer GetNodesType DHTGetNodes unsendNodes) (GetNodes nid) addr
340 -- hPutStrLn stderr $ show addr ++ " -sendnodes-> " ++ show reply
341 return $ fmap unwrapNodes $ join reply
342
343updateRouting :: Client -> Routing -> OnionRouter -> NodeInfo -> Message -> IO ()
344updateRouting client routing orouter naddr msg
345 | PacketKind 0x21 <- msgType msg = return () -- ignore lan discovery
346 | otherwise = do
347 case prefer4or6 naddr Nothing of
348 Want_IP4 -> updateTable client naddr orouter (committee4 routing) (refresher4 routing)
349 Want_IP6 -> updateTable client naddr orouter (committee6 routing) (refresher4 routing)
350 Want_Both -> error $ "BUG:unreachable at " ++ __FILE__ ++ ":" ++ show __LINE__
351
352updateTable :: Client -> NodeInfo -> OnionRouter -> TriadCommittee NodeId SockAddr -> BucketRefresher NodeId NodeInfo -> IO ()
353updateTable client naddr orouter committee refresher = do
354 self <- atomically $ R.thisNode <$> readTVar (refreshBuckets refresher)
355 when (nodeIP self /= nodeIP naddr) $ do
356 -- TODO: IP address vote?
357 insertNode (toxKademlia client committee orouter refresher) naddr
358
359toxKademlia :: Client -> TriadCommittee NodeId SockAddr -> OnionRouter
360 -> BucketRefresher NodeId NodeInfo
361 -> Kademlia NodeId NodeInfo
362toxKademlia client committee orouter refresher
363 = Kademlia quietInsertions
364 toxSpace
365 (vanillaIO (refreshBuckets refresher) $ ping client)
366 { tblTransition = \tr -> do
367 io1 <- transitionCommittee committee tr
368 io2 <- touchBucket refresher tr -- toxSpace (15*60) var sched tr
369 hookBucketList toxSpace (refreshBuckets refresher) orouter tr
370 return $ do
371 io1 >> io2
372 {-
373 hPutStrLn stderr $ unwords
374 [ show (transitionedTo tr)
375 , show (transitioningNode tr)
376 ]
377 -}
378 return ()
379 }
380
381transitionCommittee :: TriadCommittee NodeId SockAddr -> RoutingTransition NodeInfo -> STM (IO ())
382transitionCommittee committee (RoutingTransition ni Stranger) = do
383 delVote committee (nodeId ni)
384 return $ do
385 -- hPutStrLn stderr $ "delVote "++show (nodeId ni)
386 return ()
387transitionCommittee committee _ = return $ return ()
388
389type Handler = MethodHandler String TransactionId NodeInfo Message
390
391isPing :: (f Ping -> Ping) -> DHTMessage f -> Either String Ping
392isPing unpack (DHTPing a) = Right $ unpack $ asymmData a
393isPing _ _ = Left "Bad ping"
394
395mkPong :: TransactionId -> NodeInfo -> NodeInfo -> Pong -> DHTMessage ((,) Nonce8)
396mkPong tid src dst pong = DHTPong $ wrapAsymm tid src dst (, pong)
397
398isGetNodes :: (f GetNodes -> GetNodes) -> DHTMessage f -> Either String GetNodes
399isGetNodes unpack (DHTGetNodes a) = Right $ unpack $ asymmData a
400isGetNodes _ _ = Left "Bad GetNodes"
401
402mkSendNodes :: TransactionId -> NodeInfo -> NodeInfo -> SendNodes -> DHTMessage ((,) Nonce8)
403mkSendNodes tid src dst sendnodes = DHTSendNodes $ wrapAsymm tid src dst (, sendnodes)
404
405isCookieRequest :: (f CookieRequest -> CookieRequest) -> DHTMessage f -> Either String CookieRequest
406isCookieRequest unpack (DHTCookieRequest a) = Right $ unpack $ asymmData a
407isCookieRequest _ _ = Left "Bad cookie request"
408
409mkCookie :: TransactionId -> NodeInfo -> NodeInfo -> Cookie -> DHTMessage ((,) Nonce8)
410mkCookie (TransactionId n8 n24) src dst cookie = DHTCookie n24 (n8,cookie)
411
412isDHTRequest :: (f DHTRequest -> DHTRequest) -> DHTMessage f -> Either String DHTRequest
413isDHTRequest unpack (DHTDHTRequest pubkey a) = Right $ unpack $ asymmData a
414isDHTRequest _ _ = Left "Bad dht relay request"
415
416dhtRequestH :: NodeInfo -> DHTRequest -> IO ()
417dhtRequestH ni req = do
418 hPutStrLn stderr $ "Unhandled DHT Request: " ++ show req
419
420handlers :: TransportCrypto -> Routing -> PacketKind -> Maybe Handler
421handlers _ routing PingType = Just $ MethodHandler (isPing snd) mkPong $ pingH
422handlers _ routing GetNodesType = Just $ MethodHandler (isGetNodes snd) mkSendNodes $ getNodesH routing
423handlers crypto _ CookieRequestType = Just $ MethodHandler (isCookieRequest snd) mkCookie $ cookieRequestH crypto
424handlers _ _ DHTRequestType = Just $ NoReply (isDHTRequest snd) $ dhtRequestH
425handlers _ _ typ = error $ "TODO DHT handlers " ++ show typ
426
427nodeSearch :: Client -> Search NodeId (IP,PortNumber) () NodeInfo NodeInfo
428nodeSearch client = Search
429 { searchSpace = toxSpace
430 , searchNodeAddress = nodeIP &&& nodePort
431 , searchQuery = getNodes client
432 }
diff --git a/src/Network/Tox/DHT/Transport.hs b/src/Network/Tox/DHT/Transport.hs
new file mode 100644
index 00000000..2e5649d3
--- /dev/null
+++ b/src/Network/Tox/DHT/Transport.hs
@@ -0,0 +1,478 @@
1{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2{-# LANGUAGE KindSignatures #-}
3{-# LANGUAGE LambdaCase #-}
4{-# LANGUAGE RankNTypes #-}
5{-# LANGUAGE StandaloneDeriving #-}
6{-# LANGUAGE TupleSections #-}
7{-# LANGUAGE TypeOperators #-}
8{-# LANGUAGE UndecidableInstances #-}
9module Network.Tox.DHT.Transport
10 ( parseDHTAddr
11 , encodeDHTAddr
12 , forwardDHTRequests
13 , module Network.Tox.NodeId
14 , DHTMessage(..)
15 , Ping(..)
16 , Pong(..)
17 , GetNodes(..)
18 , SendNodes(..)
19 , DHTPublicKey(..)
20 , FriendRequest(..)
21 , NoSpam(..)
22 , verifyChecksum
23 , CookieRequest(..)
24 , CookieResponse(..)
25 , Cookie(..)
26 , CookieData(..)
27 , DHTRequest
28 , mapMessage
29 , encrypt
30 , decrypt
31 , dhtMessageType
32 , asymNodeInfo
33 ) where
34
35import Network.Tox.NodeId
36import Crypto.Tox hiding (encrypt,decrypt)
37import qualified Crypto.Tox as ToxCrypto
38import Network.QueryResponse
39
40import Control.Arrow
41import Control.Concurrent.STM
42import Control.Monad
43import Data.Bool
44import qualified Data.ByteString.Char8 as B8
45import qualified Data.ByteString as B
46 ;import Data.ByteString (ByteString)
47import Data.Functor.Contravariant
48import Data.Maybe
49import Data.Monoid
50import Data.Serialize as S
51import Data.Tuple
52import Data.Word
53import Foreign.C (CTime(..))
54import Network.Socket
55import qualified Data.ByteString.Base64 as Base64
56import qualified Data.ByteString.Base16 as Base16
57import Data.Char (isSpace)
58
59type DHTTransport = Transport String NodeInfo (DHTMessage Encrypted8)
60type HandleHi a = Maybe (Either String (DHTMessage Encrypted8, NodeInfo)) -> IO a
61
62
63data DHTMessage (f :: * -> *)
64 = DHTPing (Asymm (f Ping))
65 | DHTPong (Asymm (f Pong))
66 | DHTGetNodes (Asymm (f GetNodes))
67 | DHTSendNodes (Asymm (f SendNodes))
68 | DHTCookieRequest (Asymm (f CookieRequest))
69 | DHTCookie Nonce24 (f Cookie)
70 | DHTDHTRequest PublicKey (Asymm (f DHTRequest))
71 | DHTLanDiscovery NodeId
72
73deriving instance ( Show (f Cookie)
74 , Show (Asymm (f Ping))
75 , Show (Asymm (f Pong))
76 , Show (Asymm (f GetNodes))
77 , Show (Asymm (f SendNodes))
78 , Show (Asymm (f CookieRequest))
79 , Show (Asymm (f DHTRequest))
80 ) => Show (DHTMessage f)
81
82mapMessage :: forall f b. (forall a. Nonce24 -> f a -> b) -> DHTMessage f -> Maybe b
83mapMessage f (DHTPing a) = Just $ f (asymmNonce a) (asymmData a)
84mapMessage f (DHTPong a) = Just $ f (asymmNonce a) (asymmData a)
85mapMessage f (DHTGetNodes a) = Just $ f (asymmNonce a) (asymmData a)
86mapMessage f (DHTSendNodes a) = Just $ f (asymmNonce a) (asymmData a)
87mapMessage f (DHTCookieRequest a) = Just $ f (asymmNonce a) (asymmData a)
88mapMessage f (DHTDHTRequest _ a) = Just $ f (asymmNonce a) (asymmData a)
89mapMessage f (DHTCookie nonce fcookie) = Just $ f nonce fcookie
90mapMessage f (DHTLanDiscovery nid) = Nothing
91
92
93instance Sized Ping where size = ConstSize 1
94instance Sized Pong where size = ConstSize 1
95
96parseDHTAddr :: TransportCrypto -> (ByteString, SockAddr) -> IO (Either (DHTMessage Encrypted8,NodeInfo) (ByteString,SockAddr))
97parseDHTAddr crypto (msg,saddr)
98 | Just (typ,bs) <- B.uncons msg
99 , let right = return $ Right (msg,saddr)
100 left = either (const right) (return . Left)
101 = case typ of
102 0x00 -> left $ direct bs saddr DHTPing
103 0x01 -> left $ direct bs saddr DHTPong
104 0x02 -> left $ direct bs saddr DHTGetNodes
105 0x04 -> left $ direct bs saddr DHTSendNodes
106 0x18 -> left $ direct bs saddr DHTCookieRequest
107 0x19 -> do
108 cs <- atomically $ readTVar (pendingCookies crypto)
109 let ni = fromMaybe (noReplyAddr saddr) $ do
110 (cnt,key) <- lookup saddr cs
111 either (const Nothing) Just $ nodeInfo (key2id key) saddr
112 left $ fanGet bs getCookie (uncurry DHTCookie) (const $ ni)
113 0x20 -> left $ fanGet bs getDHTReqest (uncurry DHTDHTRequest) (asymNodeInfo saddr . snd)
114 0x21 -> left $ do
115 nid <- runGet get bs
116 ni <- nodeInfo nid saddr
117 return (DHTLanDiscovery nid, ni)
118 _ -> right
119
120encodeDHTAddr :: (DHTMessage Encrypted8,NodeInfo) -> IO (ByteString, SockAddr)
121encodeDHTAddr (msg,ni) = return (runPut $ putMessage msg, nodeAddr ni)
122
123dhtMessageType :: ( Serialize (f DHTRequest)
124 , Serialize (f Cookie), Serialize (f CookieRequest)
125 , Serialize (f SendNodes), Serialize (f GetNodes)
126 , Serialize (f Pong), Serialize (f Ping)
127 ) => DHTMessage f -> (Word8, Put)
128dhtMessageType (DHTPing a) = (0x00, putAsymm a)
129dhtMessageType (DHTPong a) = (0x01, putAsymm a)
130dhtMessageType (DHTGetNodes a) = (0x02, putAsymm a)
131dhtMessageType (DHTSendNodes a) = (0x04, putAsymm a)
132dhtMessageType (DHTCookieRequest a) = (0x18, putAsymm a)
133dhtMessageType (DHTCookie n x) = (0x19, put n >> put x)
134dhtMessageType (DHTDHTRequest k a) = (0x20, putPublicKey k >> putAsymm a)
135dhtMessageType (DHTLanDiscovery nid) = (0x21, put nid)
136
137putMessage :: DHTMessage Encrypted8 -> Put
138putMessage msg = case dhtMessageType msg of (typ,p) -> put typ >> p
139
140getCookie :: Get (Nonce24, Encrypted8 Cookie)
141getCookie = get
142
143getDHTReqest :: Get (PublicKey, Asymm (Encrypted8 DHTRequest))
144getDHTReqest = (,) <$> getPublicKey <*> getAsymm
145
146-- ## DHT Request packets
147--
148-- | Length | Contents |
149-- |:-------|:--------------------------|
150-- | `1` | `uint8_t` (0x20) |
151-- | `32` | receiver's DHT public key |
152-- ... ...
153
154
155getDHT :: Sized a => Get (Asymm (Encrypted8 a))
156getDHT = getAsymm
157
158
159-- Throws an error if called with a non-internet socket.
160direct :: Sized a => ByteString
161 -> SockAddr
162 -> (Asymm (Encrypted8 a) -> DHTMessage Encrypted8)
163 -> Either String (DHTMessage Encrypted8, NodeInfo)
164direct bs saddr f = fanGet bs getDHT f (asymNodeInfo saddr)
165
166-- Throws an error if called with a non-internet socket.
167asymNodeInfo :: SockAddr -> Asymm a -> NodeInfo
168asymNodeInfo saddr asym = either (error . mappend "asymNodeInfo: ") id $ nodeInfo (key2id $ senderKey asym) saddr
169
170
171fanGet :: ByteString -> Get x -> (x -> a) -> (x -> b) -> Either String (a,b)
172fanGet bs getIt f nid = fmap (f &&& nid) $ runGet getIt bs
173
174-- Throws an error if called with a non-internet socket.
175noReplyAddr :: SockAddr -> NodeInfo
176noReplyAddr saddr = either (error . mappend "noReplyAddr: ") id $ nodeInfo zeroID saddr
177
178
179data DHTRequest
180 -- #### NAT ping request
181 --
182 -- Length Contents
183 -- :------- :-------------------------
184 -- `1` `uint8_t` (0xfe)
185 -- `1` `uint8_t` (0x00)
186 -- `8` `uint64_t` random number
187 = NATPing Nonce8
188 -- #### NAT ping response
189 --
190 -- Length Contents
191 -- :------- :-----------------------------------------------------------------
192 -- `1` `uint8_t` (0xfe)
193 -- `1` `uint8_t` (0x01)
194 -- `8` `uint64_t` random number (the same that was received in request)
195 | NATPong Nonce8
196 | DHTPK LongTermKeyWrap
197 -- From docs/Hardening_docs.txt
198 --
199 -- All hardening requests must contain exactly 384 bytes of data. (The data sent
200 -- must be padded with zeros if it is smaller than that.)
201 --
202 -- [byte with value: 02 (get nodes test request)][struct Node_format (the node to
203 -- test.)][client_id(32 bytes) the id to query the node with.][padding]
204 --
205 -- packet id: CRYPTO_PACKET_HARDENING (48)
206 | Hardening -- TODO
207 deriving Show
208
209instance Sized DHTRequest where
210 size = VarSize $ \case
211 NATPing _ -> 10
212 NATPong _ -> 10
213 DHTPK wrap -> 1{-typ-} + 32{-key-} + 24{-nonce-}
214 + case size of
215 ConstSize n -> n
216 VarSize f -> f (wrapData wrap)
217 Hardening -> 1{-typ-} + 384
218
219instance Serialize DHTRequest where
220 get = do
221 tag <- get
222 case tag :: Word8 of
223 0xfe -> do
224 direction <- get
225 bool NATPong NATPing (direction==(0::Word8)) <$> get
226 0x9c -> DHTPK <$> get
227 0x30 -> pure Hardening -- TODO: CRYPTO_PACKET_HARDENING
228 _ -> fail ("unrecognized DHT request: "++show tag)
229 put (NATPing n) = put (0xfe00 :: Word16) >> put n
230 put (NATPong n) = put (0xfe01 :: Word16) >> put n
231 put (DHTPK pk) = put (0x9c :: Word8) >> put pk
232 put (Hardening) = put (0x30 :: Word8) >> putByteString (B.replicate 384 0) -- TODO
233
234-- DHT public key packet:
235-- (As Onion data packet?)
236--
237-- | Length | Contents |
238-- |:------------|:------------------------------------|
239-- | `1` | `uint8_t` (0x9c) |
240-- | `8` | `uint64_t` `no_replay` |
241-- | `32` | Our DHT public key |
242-- | `[39, 204]` | Maximum of 4 nodes in packed format |
243data DHTPublicKey = DHTPublicKey
244 { dhtpkNonce :: Word64 -- ^ The `no_replay` number is protection if
245 -- someone tries to replay an older packet and
246 -- should be set to an always increasing number.
247 -- It is 8 bytes so you should set a high
248 -- resolution monotonic time as the value.
249 , dhtpk :: PublicKey -- dht public key
250 , dhtpkNodes :: SendNodes -- other reachable nodes
251 }
252 deriving (Eq, Show)
253
254
255-- int8_t (0x20 sent over onion, 0x12 for sent over net_crypto)
256-- [uint32_t nospam][Message (UTF8) 1 to ONION_CLIENT_MAX_DATA_SIZE bytes]
257data FriendRequest = FriendRequest
258 { friendNoSpam :: Word32
259 , friendRequestText :: ByteString -- UTF8
260 }
261 deriving (Eq, Show)
262
263data NoSpam = NoSpam !Word32 !(Maybe Word16)
264
265instance Read NoSpam where
266 readsPrec d s = case break isSpace s of
267 (ws,rs) | (length ws == 8) -> base64decode rs (NoSpam <$> get <*> (Just <$> get)) ws
268 (ws,rs) | (length ws == 12) -> base16decode rs (NoSpam <$> get <*> (Just <$> get)) ws
269 _ -> []
270
271base64decode :: Monad m => t1 -> Get t -> String -> m (t, t1)
272base64decode rs getter s =
273 either fail (\a -> return (a,rs))
274 $ runGet getter
275 =<< Base64.decode (B8.pack s)
276
277base16decode :: Monad m => t1 -> Get t -> String -> m (t, t1)
278base16decode rs getter s =
279 either fail (\a -> return (a,rs))
280 $ runGet getter
281 $ fst
282 $ Base16.decode (B8.pack s)
283
284verifyChecksum :: PublicKey -> Word16 -> Either String ()
285verifyChecksum _ _ = return () -- TODO
286
287
288-- When sent as a DHT request packet (this is the data sent in the DHT request
289-- packet):
290--
291-- Length Contents
292-- :--------- :-------------------------------
293-- `1` `uint8_t` (0x9c)
294-- `32` Long term public key of sender
295-- `24` Nonce
296-- variable Encrypted payload
297data LongTermKeyWrap = LongTermKeyWrap
298 { wrapLongTermKey :: PublicKey
299 , wrapNonce :: Nonce24
300 , wrapData :: Encrypted DHTPublicKey
301 }
302 deriving Show
303
304instance Serialize LongTermKeyWrap where
305 get = LongTermKeyWrap <$> getPublicKey <*> get <*> get
306 put (LongTermKeyWrap key nonce dta) = putPublicKey key >> put nonce >> put dta
307
308
309instance Sized DHTPublicKey where
310 -- NOTE: 41 bytes includes the 1-byte tag 0x9c in the size.
311 -- WARNING: Serialize instance does not include this byte FIXME
312 size = VarSize $ \(DHTPublicKey _ _ nodes) -> 41 + case size of
313 ConstSize nodes -> nodes
314 VarSize sznodes -> sznodes nodes
315
316instance Sized Word32 where size = ConstSize 4
317
318-- FIXME: Inconsitently, this type does not include the 0x20 or 0x12 tag byte
319-- where the DHTPublicKey type does include its tag.
320instance Sized FriendRequest where
321 size = contramap friendNoSpam size <> contramap friendRequestText (VarSize B.length)
322
323instance Serialize DHTPublicKey where
324 -- TODO: This should agree with Sized instance.
325 get = DHTPublicKey <$> get <*> getPublicKey <*> get
326 put (DHTPublicKey nonce key nodes) = do
327 put nonce
328 putPublicKey key
329 put nodes
330
331instance Serialize FriendRequest where
332 get = FriendRequest <$> get <*> (remaining >>= getBytes)
333 put (FriendRequest nospam txt) = put nospam >> putByteString txt
334
335newtype GetNodes = GetNodes NodeId
336 deriving (Eq,Ord,Show,Read,S.Serialize)
337
338instance Sized GetNodes where
339 size = ConstSize 32 -- TODO This right?
340
341newtype SendNodes = SendNodes [NodeInfo]
342 deriving (Eq,Ord,Show,Read)
343
344instance Sized SendNodes where
345 size = VarSize $ \(SendNodes ns) -> case size of
346 ConstSize nodeFormatSize -> nodeFormatSize * length ns
347 VarSize nsize -> sum $ map nsize ns
348
349instance S.Serialize SendNodes where
350 get = do
351 cnt <- S.get :: S.Get Word8
352 ns <- sequence $ replicate (fromIntegral cnt) S.get
353 return $ SendNodes ns
354
355 put (SendNodes ns) = do
356 let ns' = take 4 ns
357 S.put (fromIntegral (length ns') :: Word8)
358 mapM_ S.put ns'
359
360data Ping = Ping deriving Show
361data Pong = Pong deriving Show
362
363instance S.Serialize Ping where
364 get = do w8 <- S.get
365 if (w8 :: Word8) /= 0
366 then fail "Malformed ping."
367 else return Ping
368 put Ping = S.put (0 :: Word8)
369
370instance S.Serialize Pong where
371 get = do w8 <- S.get
372 if (w8 :: Word8) /= 1
373 then fail "Malformed pong."
374 else return Pong
375 put Pong = S.put (1 :: Word8)
376
377newtype CookieRequest = CookieRequest PublicKey
378 deriving (Eq, Show)
379newtype CookieResponse = CookieResponse Cookie
380 deriving (Eq, Show)
381
382data Cookie = Cookie Nonce24 (Encrypted CookieData)
383 deriving (Eq, Ord, Show)
384
385instance Sized Cookie where size = ConstSize 112 -- 24 byte nonce + 88 byte cookie data
386
387instance Serialize Cookie where
388 get = Cookie <$> get <*> get
389 put (Cookie nonce dta) = put nonce >> put dta
390
391data CookieData = CookieData -- 16 (mac)
392 { cookieTime :: Word64 -- 8
393 , longTermKey :: PublicKey -- 32
394 , dhtKey :: PublicKey -- + 32
395 } -- = 88 bytes when encrypted.
396
397instance Sized CookieData where
398 size = ConstSize 72
399
400instance Serialize CookieData where
401 get = CookieData <$> get
402 <*> (id2key <$> get)
403 <*> (id2key <$> get)
404 put (CookieData tm userkey dhtkey) = do
405 put tm
406 put (key2id userkey)
407 put (key2id dhtkey)
408
409instance Sized CookieRequest where
410 size = ConstSize 64 -- 32 byte key + 32 byte padding
411
412instance Serialize CookieRequest where
413 get = CookieRequest <$> getPublicKey <* {- padding -} getPublicKey
414 put (CookieRequest k) = putPublicKey k >> {- padding -} putPublicKey k
415
416forwardDHTRequests :: TransportCrypto -> (PublicKey -> IO (Maybe NodeInfo)) -> DHTTransport -> DHTTransport
417forwardDHTRequests crypto closeLookup dht = dht { awaitMessage = await' }
418 where
419 await' :: HandleHi a -> IO a
420 await' pass = awaitMessage dht $ \case
421 Just (Right (m@(DHTDHTRequest target payload),src)) | target /= transportPublic crypto
422 -> do mni <- closeLookup target
423 -- Forward the message if the target is in our close list.
424 forM_ mni $ \ni -> sendMessage dht ni m
425 await' pass
426 m -> pass m
427
428encrypt :: TransportCrypto -> DHTMessage ((,) Nonce8) -> NodeInfo -> IO (DHTMessage Encrypted8, NodeInfo)
429encrypt crypto msg ni = do
430 let cipher n plain = Composed $ encryptMessage crypto (id2key $ nodeId ni) n plain
431 m <- sequenceMessage $ transcode cipher msg
432 return (m, ni)
433
434encryptMessage :: Serialize a =>
435 TransportCrypto ->
436 PublicKey ->
437 Nonce24 -> Either (Nonce8,a) (Asymm (Nonce8,a)) -> IO (Encrypted8 a)
438encryptMessage crypto destKey n arg = do
439 let plain = encodePlain $ swap $ either id asymmData arg
440 secret <- lookupSharedSecret crypto (transportSecret crypto) destKey n
441 return $ E8 $ ToxCrypto.encrypt secret plain
442
443decrypt :: TransportCrypto -> DHTMessage Encrypted8 -> NodeInfo -> IO (Either String (DHTMessage ((,) Nonce8), NodeInfo))
444decrypt crypto msg ni = do
445 let decipher n c = Composed $ decryptMessage crypto n . left ((,) $ id2key $ nodeId ni) $ c
446 msg' <- sequenceMessage $ transcode decipher msg
447 return $ fmap (, ni) $ sequenceMessage msg'
448
449decryptMessage :: Serialize x =>
450 TransportCrypto
451 -> Nonce24
452 -> Either (PublicKey, Encrypted8 x) (Asymm (Encrypted8 x))
453 -> IO ((Either String ∘ ((,) Nonce8)) x)
454decryptMessage crypto n arg = do
455 let (remotekey,E8 e) = either id (senderKey &&& asymmData) arg
456 plain8 = Composed . fmap swap . (>>= decodePlain)
457 secret <- lookupSharedSecret crypto (transportSecret crypto) remotekey n
458 return $ plain8 $ ToxCrypto.decrypt secret e
459
460sequenceMessage :: Applicative m => DHTMessage (m ∘ f) -> m (DHTMessage f)
461sequenceMessage (DHTPing asym) = fmap DHTPing $ sequenceA $ fmap uncomposed asym
462sequenceMessage (DHTPong asym) = fmap DHTPong $ sequenceA $ fmap uncomposed asym
463sequenceMessage (DHTGetNodes asym) = fmap DHTGetNodes $ sequenceA $ fmap uncomposed asym
464sequenceMessage (DHTSendNodes asym) = fmap DHTSendNodes $ sequenceA $ fmap uncomposed asym
465sequenceMessage (DHTCookieRequest asym) = fmap DHTCookieRequest $ sequenceA $ fmap uncomposed asym
466sequenceMessage (DHTCookie n dta) = fmap (DHTCookie n) $ uncomposed dta
467sequenceMessage (DHTDHTRequest pubkey asym) = fmap (DHTDHTRequest pubkey) $ sequenceA $ fmap uncomposed asym
468sequenceMessage (DHTLanDiscovery nid) = pure $ DHTLanDiscovery nid
469
470transcode :: forall f g. (forall a. Serialize a => Nonce24 -> Either (f a) (Asymm (f a)) -> g a) -> DHTMessage f -> DHTMessage g
471transcode f (DHTPing asym) = DHTPing $ asym { asymmData = f (asymmNonce asym) (Right asym) }
472transcode f (DHTPong asym) = DHTPong $ asym { asymmData = f (asymmNonce asym) (Right asym) }
473transcode f (DHTGetNodes asym) = DHTGetNodes $ asym { asymmData = f (asymmNonce asym) (Right asym) }
474transcode f (DHTSendNodes asym) = DHTSendNodes $ asym { asymmData = f (asymmNonce asym) (Right asym) }
475transcode f (DHTCookieRequest asym) = DHTCookieRequest $ asym { asymmData = f (asymmNonce asym) (Right asym) }
476transcode f (DHTCookie n dta) = DHTCookie n $ f n $ Left dta
477transcode f (DHTDHTRequest pubkey asym) = DHTDHTRequest pubkey $ asym { asymmData = f (asymmNonce asym) (Right asym) }
478transcode f (DHTLanDiscovery nid) = DHTLanDiscovery nid
diff --git a/src/Network/Tox/NodeId.hs b/src/Network/Tox/NodeId.hs
new file mode 100644
index 00000000..95604108
--- /dev/null
+++ b/src/Network/Tox/NodeId.hs
@@ -0,0 +1,470 @@
1{- LANGUAGE ApplicativeDo -}
2{-# LANGUAGE BangPatterns #-}
3{-# LANGUAGE CPP #-}
4{-# LANGUAGE DataKinds #-}
5{-# LANGUAGE DeriveDataTypeable #-}
6{-# LANGUAGE DeriveFunctor #-}
7{-# LANGUAGE DeriveTraversable #-}
8{-# LANGUAGE ExistentialQuantification #-}
9{-# LANGUAGE FlexibleInstances #-}
10{-# LANGUAGE GADTs #-}
11{-# LANGUAGE GeneralizedNewtypeDeriving #-}
12{-# LANGUAGE KindSignatures #-}
13{-# LANGUAGE PatternSynonyms #-}
14{-# LANGUAGE ScopedTypeVariables #-}
15{-# LANGUAGE TupleSections #-}
16{- LANGUAGE TypeApplications -}
17module Network.Tox.NodeId
18 ( NodeInfo(..)
19 , NodeId
20 , nodeInfo
21 , nodeAddr
22 , zeroID
23 , key2id
24 , id2key
25 , getIP
26 , xorNodeId
27 , testNodeIdBit
28 , sampleNodeId) where
29
30import Control.Applicative
31import Control.Arrow
32import Control.Monad
33import Crypto.Error.Types (CryptoFailable (..),
34 throwCryptoError)
35import Crypto.PubKey.Curve25519
36import qualified Data.Aeson as JSON
37 ;import Data.Aeson (FromJSON, ToJSON, (.=))
38import Data.Bits.ByteString ()
39import qualified Data.ByteArray as BA
40 ;import Data.ByteArray as BA (ByteArrayAccess)
41import qualified Data.ByteString as B
42 ;import Data.ByteString (ByteString)
43import qualified Data.ByteString.Base16 as Base16
44import qualified Data.ByteString.Base64 as Base64
45import qualified Data.ByteString.Char8 as C8
46import Data.Char
47import Data.Data
48import Data.Hashable
49import Data.IP
50import Data.Serialize as S
51import Data.Word
52import Foreign.Storable
53import GHC.TypeLits
54import Network.Address hiding (nodePort)
55import System.IO.Unsafe (unsafeDupablePerformIO)
56import qualified Text.ParserCombinators.ReadP as RP
57import Text.Read
58import Data.Bits
59import Crypto.Tox
60import Foreign.Ptr
61import Data.Function
62import System.Endian
63
64-- | perform io for hashes that do allocation and ffi.
65-- unsafeDupablePerformIO is used when possible as the
66-- computation is pure and the output is directly linked
67-- to the input. we also do not modify anything after it has
68-- been returned to the user.
69unsafeDoIO :: IO a -> a
70#if __GLASGOW_HASKELL__ > 704
71unsafeDoIO = unsafeDupablePerformIO
72#else
73unsafeDoIO = unsafePerformIO
74#endif
75
76unpackPublicKey :: ByteArrayAccess bs => bs -> [Word64]
77unpackPublicKey bs = loop 0
78 where loop i
79 | i == (BA.length bs `div` 8) = []
80 | otherwise =
81 let !v = unsafeDoIO $ BA.withByteArray bs (\p -> fromBE64 <$> peekElemOff p i)
82 in v : loop (i+1)
83
84packPublicKey :: BA.ByteArray bs => [Word64] -> bs
85packPublicKey ws = BA.allocAndFreeze (8 * length ws) $
86 flip fix ws $ \loop ys ptr -> case ys of
87 [] -> return ()
88 x:xs -> do poke ptr (toBE64 x)
89 loop xs (plusPtr ptr 8)
90
91-- We represent the node id redundantly in two formats. The [Word64] format is
92-- convenient for short-circuiting xor/distance comparisons. The PublicKey
93-- format is convenient for encryption.
94data NodeId = NodeId [Word64] !(Maybe PublicKey)
95
96instance Eq NodeId where
97 (NodeId ws _) == (NodeId xs _)
98 = ws == xs
99
100instance Ord NodeId where
101 compare (NodeId ws _) (NodeId xs _) = compare ws xs
102
103instance Sized NodeId where size = ConstSize 32
104
105key2id :: PublicKey -> NodeId
106key2id k = NodeId (unpackPublicKey k) (Just k)
107
108bs2id :: ByteString -> NodeId
109bs2id bs = uncurry NodeId . (unpackPublicKey &&& Just) $ throwCryptoError . publicKey $ bs
110
111id2key :: NodeId -> PublicKey
112id2key (NodeId ws (Just key)) = key
113id2key (NodeId key Nothing) = throwCryptoError . publicKey $ (packPublicKey key :: BA.Bytes)
114
115zeroKey :: PublicKey
116zeroKey = throwCryptoError $ publicKey $ B.replicate 32 0
117
118zeroID :: NodeId
119zeroID = NodeId (replicate 4 0) (Just zeroKey)
120
121-- | Convert to and from a Base64 variant that uses .- instead of +/.
122nmtoken64 :: Bool -> Char -> Char
123nmtoken64 False '.' = '+'
124nmtoken64 False '-' = '/'
125nmtoken64 True '+' = '.'
126nmtoken64 True '/' = '-'
127nmtoken64 _ c = c
128
129instance Read NodeId where
130 readsPrec _ str
131 | Right bs <- fmap (BA.drop 1) $ Base64.decode $ C8.pack $ 'A':map (nmtoken64 False) (take 43 str)
132 , CryptoPassed pub <- publicKey bs -- B.length bs == 32
133 = [ (key2id pub, drop 43 str) ]
134 | otherwise = []
135
136instance Show NodeId where
137 show nid = map (nmtoken64 True) $ C8.unpack $ BA.drop 1 $ Base64.encode $ BA.cons 0 $ BA.convert $ id2key nid
138
139instance S.Serialize NodeId where
140 get = key2id <$> getPublicKey
141 put nid = putPublicKey $ id2key nid
142
143instance Hashable NodeId where
144 hashWithSalt salt (NodeId ws _) = hashWithSalt salt (head ws)
145
146testNodeIdBit :: NodeId -> Word -> Bool
147testNodeIdBit (NodeId ws _) i -- TODO: Optmize: use ByteArray key if it's available.
148 | fromIntegral i < 256 -- 256 bits
149 , (q, r) <- quotRem (fromIntegral i) 64
150 = testBit (ws !! q) (63 - r)
151 | otherwise = False
152
153xorNodeId :: NodeId -> NodeId -> NodeId
154xorNodeId (NodeId xs _) (NodeId ys _) = NodeId (zipWith xor xs ys) Nothing
155
156sampleNodeId :: Applicative m => (Int -> m ByteString) -> NodeId -> (Int,Word8,Word8) -> m NodeId
157sampleNodeId gen (NodeId self k) (q,m,b)
158 | q <= 0 = bs2id <$> gen 32
159 | q >= 32 = pure (NodeId self k)
160 | let (qw,r) = (q+7) `divMod` 8 -- How many Word64 to prepend?
161 bw = shiftL (fromIntegral b) (8*(7-r))
162 mw = bw - 1 :: Word64
163 (hd, t0 : _) = splitAt (qw-1) self
164 h = xor bw (complement mw .&. t0)
165 = flip fmap (gen $ 8 * (4 - (qw-1)) ) $ \bs ->
166 let (w:ws) = unpackPublicKey bs
167 in NodeId (hd ++ (h .|. (w .&. mw)) : ws) Nothing
168
169data NodeInfo = NodeInfo
170 { nodeId :: NodeId
171 , nodeIP :: IP
172 , nodePort :: PortNumber
173 }
174 deriving (Eq,Ord)
175
176nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo
177nodeInfo nid saddr
178 | Just ip <- fromSockAddr saddr
179 , Just port <- sockAddrPort saddr = Right $ NodeInfo nid ip port
180 | otherwise = Left "Address family not supported."
181
182
183instance ToJSON NodeInfo where
184 toJSON (NodeInfo nid (IPv4 ip) port)
185 = JSON.object [ "public_key" .= show nid
186 , "ipv4" .= show ip
187 , "port" .= (fromIntegral port :: Int)
188 ]
189 toJSON (NodeInfo nid (IPv6 ip6) port)
190 | Just ip <- un4map ip6
191 = JSON.object [ "public_key" .= show nid
192 , "ipv4" .= show ip
193 , "port" .= (fromIntegral port :: Int)
194 ]
195 | otherwise
196 = JSON.object [ "public_key" .= show nid
197 , "ipv6" .= show ip6
198 , "port" .= (fromIntegral port :: Int)
199 ]
200instance FromJSON NodeInfo where
201 parseJSON (JSON.Object v) = do
202 nidstr <- v JSON..: "public_key"
203 ip6str <- v JSON..:? "ipv6"
204 ip4str <- v JSON..:? "ipv4"
205 portnum <- v JSON..: "port"
206 ip <- maybe empty (return . IPv6) (ip6str >>= readMaybe)
207 <|> maybe empty (return . IPv4) (ip4str >>= readMaybe)
208 let (bs,_) = Base16.decode (C8.pack nidstr)
209 enid = Base64.decode (C8.pack $ 'A' : map (nmtoken64 False) nidstr)
210 idbs <- (guard (B.length bs == 32) >> return bs)
211 <|> either fail (return . B.drop 1) enid
212 return $ NodeInfo (bs2id idbs) ip (fromIntegral (portnum :: Word16))
213
214getIP :: Word8 -> S.Get IP
215getIP 0x02 = IPv4 <$> S.get
216getIP 0x0a = IPv6 <$> S.get
217getIP 0x82 = IPv4 <$> S.get -- TODO: TCP
218getIP 0x8a = IPv6 <$> S.get -- TODO: TCP
219getIP x = fail ("unsupported address family ("++show x++")")
220
221instance Sized NodeInfo where
222 size = VarSize $ \(NodeInfo nid ip port) ->
223 case ip of
224 IPv4 _ -> 39 -- 35 + 4 = 1 + 4 + 2 + 32
225 IPv6 _ -> 51 -- 35 + 16 = 1 + 16 + 2 + 32
226
227instance S.Serialize NodeInfo where
228 get = do
229 addrfam <- S.get :: S.Get Word8
230 let fallback = do -- FIXME: Handle unrecognized address families.
231 IPv6 <$> S.get
232 return $ IPv6 (read "::" :: IPv6)
233 ip <- getIP addrfam <|> fallback
234 port <- S.get :: S.Get PortNumber
235 nid <- S.get
236 return $ NodeInfo nid ip port
237
238 put (NodeInfo nid ip port) = do
239 case ip of
240 IPv4 ip4 -> S.put (2 :: Word8) >> S.put ip4
241 IPv6 ip6 -> S.put (10 :: Word8) >> S.put ip6
242 S.put port
243 S.put nid
244
245hexdigit :: Char -> Bool
246hexdigit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'f') || ( 'A' <= c && c <= 'F')
247
248b64digit :: Char -> Bool
249b64digit '.' = True
250b64digit '+' = True
251b64digit '-' = True
252b64digit '/' = True
253b64digit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'z') || ( 'A' <= c && c <= 'Z')
254
255instance Read NodeInfo where
256 readsPrec i = RP.readP_to_S $ do
257 RP.skipSpaces
258 let n = 43 -- characters in node id.
259 parseAddr = RP.between (RP.char '(') (RP.char ')') (RP.munch (/=')'))
260 RP.+++ RP.munch (not . isSpace)
261 nodeidAt = do hexhash <- sequence $ replicate n (RP.satisfy b64digit)
262 RP.char '@' RP.+++ RP.satisfy isSpace
263 addrstr <- parseAddr
264 nid <- case Base64.decode $ C8.pack $ 'A' : map (nmtoken64 False) hexhash of
265 Right bs | B.length bs - 1==32 -> return (bs2id $ BA.drop 1 bs)
266 _ -> fail "Bad node id."
267 return (nid,addrstr)
268 (nid,addrstr) <- ( nodeidAt RP.+++ ( (zeroID,) <$> parseAddr) )
269 let raddr = do
270 ip <- RP.between (RP.char '[') (RP.char ']')
271 (IPv6 <$> RP.readS_to_P (readsPrec i))
272 RP.+++ (IPv4 <$> RP.readS_to_P (readsPrec i))
273 _ <- RP.char ':'
274 port <- toEnum <$> RP.readS_to_P (readsPrec i)
275 return (ip, port)
276
277 (ip,port) <- case RP.readP_to_S raddr addrstr of
278 [] -> fail "Bad address."
279 ((ip,port),_):_ -> return (ip,port)
280 return $ NodeInfo nid ip port
281
282-- The Hashable instance depends only on the IP address and port number.
283instance Hashable NodeInfo where
284 hashWithSalt s ni = hashWithSalt s (nodeIP ni , nodePort ni)
285 {-# INLINE hashWithSalt #-}
286
287
288instance Show NodeInfo where
289 showsPrec _ (NodeInfo nid ip port) =
290 shows nid . ('@' :) . showsip . (':' :) . shows port
291 where
292 showsip
293 | IPv4 ip4 <- ip = shows ip4
294 | IPv6 ip6 <- ip , Just ip4 <- un4map ip6 = shows ip4
295 | otherwise = ('[' :) . shows ip . (']' :)
296
297
298
299
300{-
301type NodeId = PubKey
302
303pattern NodeId bs = PubKey bs
304
305-- TODO: This should probably be represented by Curve25519.PublicKey, but
306-- ByteString has more instances...
307newtype PubKey = PubKey ByteString
308 deriving (Eq,Ord,Data, ByteArrayAccess, Bits, Hashable)
309
310instance Serialize PubKey where
311 get = PubKey <$> getBytes 32
312 put (PubKey bs) = putByteString bs
313
314instance Show PubKey where
315 show (PubKey bs) = C8.unpack $ Base16.encode bs
316
317instance FiniteBits PubKey where
318 finiteBitSize _ = 256
319
320instance Read PubKey where
321 readsPrec _ str
322 | (bs, xs) <- Base16.decode $ C8.pack str
323 , B.length bs == 32
324 = [ (PubKey bs, drop 64 str) ]
325 | otherwise = []
326
327
328
329
330data NodeInfo = NodeInfo
331 { nodeId :: NodeId
332 , nodeIP :: IP
333 , nodePort :: PortNumber
334 }
335 deriving (Eq,Ord,Data)
336
337instance Data PortNumber where
338 dataTypeOf _ = mkNoRepType "PortNumber"
339 toConstr _ = error "PortNumber.toConstr"
340 gunfold _ _ = error "PortNumber.gunfold"
341
342instance ToJSON NodeInfo where
343 toJSON (NodeInfo nid (IPv4 ip) port)
344 = JSON.object [ "public_key" .= show nid
345 , "ipv4" .= show ip
346 , "port" .= (fromIntegral port :: Int)
347 ]
348 toJSON (NodeInfo nid (IPv6 ip6) port)
349 | Just ip <- un4map ip6
350 = JSON.object [ "public_key" .= show nid
351 , "ipv4" .= show ip
352 , "port" .= (fromIntegral port :: Int)
353 ]
354 | otherwise
355 = JSON.object [ "public_key" .= show nid
356 , "ipv6" .= show ip6
357 , "port" .= (fromIntegral port :: Int)
358 ]
359instance FromJSON NodeInfo where
360 parseJSON (JSON.Object v) = do
361 nidstr <- v JSON..: "public_key"
362 ip6str <- v JSON..:? "ipv6"
363 ip4str <- v JSON..:? "ipv4"
364 portnum <- v JSON..: "port"
365 ip <- maybe empty (return . IPv6) (ip6str >>= readMaybe)
366 <|> maybe empty (return . IPv4) (ip4str >>= readMaybe)
367 let (bs,_) = Base16.decode (C8.pack nidstr)
368 guard (B.length bs == 32)
369 return $ NodeInfo (NodeId bs) ip (fromIntegral (portnum :: Word16))
370
371getIP :: Word8 -> S.Get IP
372getIP 0x02 = IPv4 <$> S.get
373getIP 0x0a = IPv6 <$> S.get
374getIP 0x82 = IPv4 <$> S.get -- TODO: TCP
375getIP 0x8a = IPv6 <$> S.get -- TODO: TCP
376getIP x = fail ("unsupported address family ("++show x++")")
377
378instance S.Serialize NodeInfo where
379 get = do
380 addrfam <- S.get :: S.Get Word8
381 ip <- getIP addrfam
382 port <- S.get :: S.Get PortNumber
383 nid <- S.get
384 return $ NodeInfo nid ip port
385
386 put (NodeInfo nid ip port) = do
387 case ip of
388 IPv4 ip4 -> S.put (2 :: Word8) >> S.put ip4
389 IPv6 ip6 -> S.put (10 :: Word8) >> S.put ip6
390 S.put port
391 S.put nid
392
393-- node format:
394-- [uint8_t family (2 == IPv4, 10 == IPv6, 130 == TCP IPv4, 138 == TCP IPv6)]
395-- [ip (in network byte order), length=4 bytes if ipv4, 16 bytes if ipv6]
396-- [port (in network byte order), length=2 bytes]
397-- [char array (node_id), length=32 bytes]
398--
399
400
401hexdigit :: Char -> Bool
402hexdigit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'f') || ( 'A' <= c && c <= 'F')
403
404instance Read NodeInfo where
405 readsPrec i = RP.readP_to_S $ do
406 RP.skipSpaces
407 let n = 64 -- characters in node id.
408 parseAddr = RP.between (RP.char '(') (RP.char ')') (RP.munch (/=')'))
409 RP.+++ RP.munch (not . isSpace)
410 nodeidAt = do hexhash <- sequence $ replicate n (RP.satisfy hexdigit)
411 RP.char '@' RP.+++ RP.satisfy isSpace
412 addrstr <- parseAddr
413 nid <- case Base16.decode $ C8.pack hexhash of
414 (bs,_) | B.length bs==32 -> return (PubKey bs)
415 _ -> fail "Bad node id."
416 return (nid,addrstr)
417 (nid,addrstr) <- ( nodeidAt RP.+++ ( (zeroID,) <$> parseAddr) )
418 let raddr = do
419 ip <- RP.between (RP.char '[') (RP.char ']')
420 (IPv6 <$> RP.readS_to_P (readsPrec i))
421 RP.+++ (IPv4 <$> RP.readS_to_P (readsPrec i))
422 _ <- RP.char ':'
423 port <- toEnum <$> RP.readS_to_P (readsPrec i)
424 return (ip, port)
425
426 (ip,port) <- case RP.readP_to_S raddr addrstr of
427 [] -> fail "Bad address."
428 ((ip,port),_):_ -> return (ip,port)
429 return $ NodeInfo nid ip port
430
431
432-- The Hashable instance depends only on the IP address and port number.
433instance Hashable NodeInfo where
434 hashWithSalt s ni = hashWithSalt s (nodeIP ni , nodePort ni)
435 {-# INLINE hashWithSalt #-}
436
437
438instance Show NodeInfo where
439 showsPrec _ (NodeInfo nid ip port) =
440 shows nid . ('@' :) . showsip . (':' :) . shows port
441 where
442 showsip
443 | IPv4 ip4 <- ip = shows ip4
444 | IPv6 ip6 <- ip , Just ip4 <- un4map ip6 = shows ip4
445 | otherwise = ('[' :) . shows ip . (']' :)
446
447nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo
448nodeInfo nid saddr
449 | Just ip <- fromSockAddr saddr
450 , Just port <- sockAddrPort saddr = Right $ NodeInfo nid ip port
451 | otherwise = Left "Address family not supported."
452
453zeroID :: NodeId
454zeroID = PubKey $ B.replicate 32 0
455
456-}
457
458nodeAddr :: NodeInfo -> SockAddr
459nodeAddr (NodeInfo _ ip port) = setPort port $ toSockAddr ip
460
461
462newtype ForwardPath (n::Nat) = ForwardPath ByteString
463 deriving (Eq, Ord,Data)
464
465{-
466class KnownNat n => OnionPacket n where
467 mkOnion :: ReturnPath n -> Packet -> Packet
468instance OnionPacket 0 where mkOnion _ = id
469instance OnionPacket 3 where mkOnion = OnionResponse3
470-}
diff --git a/src/Network/Tox/Onion/Handlers.hs b/src/Network/Tox/Onion/Handlers.hs
new file mode 100644
index 00000000..e792aa50
--- /dev/null
+++ b/src/Network/Tox/Onion/Handlers.hs
@@ -0,0 +1,279 @@
1{-# LANGUAGE LambdaCase #-}
2{-# LANGUAGE PatternSynonyms #-}
3module Network.Tox.Onion.Handlers where
4
5import Network.Kademlia.Search
6import Network.Tox.DHT.Transport
7import Network.Tox.DHT.Handlers hiding (Message,Client)
8import Network.Tox.Onion.Transport
9import Network.QueryResponse as QR hiding (Client)
10import qualified Network.QueryResponse as QR (Client)
11import Crypto.Tox
12import qualified Data.Wrapper.PSQ as PSQ
13 ;import Data.Wrapper.PSQ (PSQ)
14import Crypto.Error.Types (CryptoFailable (..),
15 throwCryptoError)
16import Control.Arrow
17
18import System.IO
19import qualified Data.ByteArray as BA
20import Data.Function
21import Data.Serialize as S
22import qualified Data.Wrapper.PSQInt as Int
23import Network.Kademlia
24import Network.Address (WantIP (..), ipFamily, testIdBit)
25import qualified Network.Kademlia.Routing as R
26import Control.TriadCommittee
27import qualified Data.MinMaxPSQ as MinMaxPSQ
28 ;import Data.MinMaxPSQ (MinMaxPSQ')
29import Network.BitTorrent.DHT.Token as Token
30
31import Control.Exception hiding (Handler)
32import Control.Monad
33import Control.Concurrent.STM
34import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime)
35import Network.Socket
36import Data.IP
37import Data.Maybe
38import Data.Bits
39import Data.Ord
40import Data.Functor.Identity
41
42type Client r = QR.Client String PacketKind TransactionId (OnionDestination r) Message
43type Message = OnionMessage Identity
44
45classify :: Message -> MessageClass String PacketKind TransactionId (OnionDestination r) Message
46classify msg = go msg
47 where
48 go (OnionAnnounce announce) = IsQuery AnnounceType
49 $ TransactionId (snd $ runIdentity $ asymmData announce)
50 (asymmNonce announce)
51 go (OnionAnnounceResponse n8 n24 resp) = IsResponse (TransactionId n8 n24)
52 go (OnionToRoute {}) = IsQuery DataRequestType (TransactionId (Nonce8 0) (Nonce24 zeros24))
53 go (OnionToRouteResponse {}) = IsResponse (TransactionId (Nonce8 0) (Nonce24 zeros24))
54
55-- Toxcore generates `ping_id`s by taking a 32 byte sha hash of the current time,
56-- some secret bytes generated when the instance is created, the current time
57-- divided by a 20 second timeout, the public key of the requester and the source
58-- ip/port that the packet was received from. Since the ip/port that the packet
59-- was received from is in the `ping_id`, the announce packets being sent with a
60-- ping id must be sent using the same path as the packet that we received the
61-- `ping_id` from or announcing will fail.
62--
63-- The reason for this 20 second timeout in toxcore is that it gives a reasonable
64-- time (20 to 40 seconds) for a peer to announce himself while taking in count
65-- all the possible delays with some extra seconds.
66announceH :: Routing -> TVar SessionTokens -> TVar AnnouncedKeys -> OnionDestination r -> AnnounceRequest -> IO AnnounceResponse
67announceH routing toks keydb oaddr req = do
68 case () of
69 _ | announcePingId req == zeros32
70 -> go False
71
72 _ -> let Nonce32 bs = announcePingId req
73 tok = fromPaddedByteString 32 bs
74 in checkToken toks (onionNodeInfo oaddr) tok >>= go
75 `catch` (\(SomeException e) -> hPutStrLn stderr ("announceH Exception! "++show e) >> throw e)
76 where
77 go withTok = do
78 let naddr = onionNodeInfo oaddr
79 ns <- getNodesH routing naddr (GetNodes (announceSeeking req))
80 tm <- getPOSIXTime
81
82 let storing = case oaddr of
83 OnionToOwner _ pth -> guard (nodeId naddr == announceSeeking req) >> Just pth
84 _ -> Nothing
85 hPutStrLn stderr $ unlines [ "announceH: nodeId = " ++ show (nodeId naddr)
86 , " announceSeeking = " ++ show (announceSeeking req)
87 , " withTok = " ++ show withTok
88 , " storing = " ++ maybe "False" (const "True") storing
89 ]
90 record <- atomically $ do
91 forM_ storing $ \retpath -> when withTok $ do
92 let toxpath = AnnouncedRoute naddr{ nodeId = announceKey req } retpath
93 -- Note: The following distance calculation assumes that
94 -- our nodeid doesn't change and is the same for both
95 -- routing4 and routing6.
96 d = xorNodeId (nodeId (tentativeId routing))
97 (announceSeeking req)
98 modifyTVar' keydb (insertKey tm (announceSeeking req) toxpath d)
99 ks <- readTVar keydb
100 return $ snd . snd <$> MinMaxPSQ.lookup' (announceSeeking req) (keyAssoc ks)
101 newtok <- maybe (return $ zeros32)
102 (const $ Nonce32 . toPaddedByteString 32 <$> grantToken toks naddr)
103 storing
104 let k = case record of
105 Nothing -> NotStored newtok
106 Just _ | isJust storing -> Acknowledged newtok
107 Just (AnnouncedRoute ni _) -> SendBackKey $ id2key (nodeId ni)
108 let response = AnnounceResponse k ns
109 hPutStrLn stderr $ unwords ["Announce:", show req, "-reply->", show response]
110 return response
111
112dataToRouteH ::
113 TVar AnnouncedKeys
114 -> Transport err (OnionDestination r) (OnionMessage f)
115 -> addr
116 -> OnionMessage f
117 -> IO ()
118dataToRouteH keydb udp _ (OnionToRoute pub asymm) = do
119 let k = key2id pub
120 hPutStrLn stderr $ "dataToRouteH "++ show k
121 mb <- atomically $ do
122 ks <- readTVar keydb
123 forM (MinMaxPSQ.lookup' k (keyAssoc ks)) $ \(p,(cnt,rpath)) -> do
124 writeTVar keydb $ ks { keyAssoc = MinMaxPSQ.insert' k (cnt + 1, rpath) p (keyAssoc ks) }
125 return rpath
126 hPutStrLn stderr $ "dataToRouteH "++ show (fmap (const ()) mb)
127 forM_ mb $ \rpath -> do
128 -- forward
129 hPutStrLn stderr $ "dataToRouteH sendMessage"
130 sendMessage udp (toOnionDestination rpath) $ OnionToRouteResponse asymm
131 hPutStrLn stderr $ "Forwarding data-to-route -->"++show k
132
133type NodeDistance = NodeId
134
135data AnnouncedRoute = AnnouncedRoute NodeInfo (ReturnPath N3)
136
137toOnionDestination :: AnnouncedRoute -> OnionDestination r
138toOnionDestination (AnnouncedRoute ni rpath) = OnionToOwner ni rpath
139
140data AnnouncedKeys = AnnouncedKeys
141 { keyByAge :: !(PSQ NodeId (Down POSIXTime)) -- TODO: timeout of 300 seconds
142 , keyAssoc :: !(MinMaxPSQ' NodeId NodeDistance (Int,AnnouncedRoute))
143 }
144
145
146insertKey :: POSIXTime -> NodeId -> AnnouncedRoute -> NodeDistance -> AnnouncedKeys -> AnnouncedKeys
147insertKey tm pub toxpath d keydb = AnnouncedKeys
148 { keyByAge = PSQ.insert pub (Down tm) (keyByAge keydb)
149 , keyAssoc = case MinMaxPSQ.lookup' pub (keyAssoc keydb) of
150 Just (_,(cnt,_)) -> MinMaxPSQ.insert' pub (cnt,toxpath) d (keyAssoc keydb)
151 Nothing -> MinMaxPSQ.insert' pub (0 ,toxpath) d (keyAssoc keydb)
152 }
153
154areq :: Message -> Either String AnnounceRequest
155areq (OnionAnnounce asymm) = Right $ fst $ runIdentity $ asymmData asymm
156areq _ = Left "Unexpected non-announce OnionMessage"
157
158handlers :: Transport err (OnionDestination r) Message
159 -> Routing
160 -> TVar SessionTokens
161 -> TVar AnnouncedKeys
162 -> PacketKind
163 -> Maybe (MethodHandler String TransactionId (OnionDestination r) Message)
164handlers net routing toks keydb AnnounceType
165 = Just
166 $ MethodHandler areq (\(TransactionId n8 n24) src dst -> OnionAnnounceResponse n8 n24 . Identity)
167 $ announceH routing toks keydb
168handlers net _ _ keydb _ = Just $ NoReply Right $ dataToRouteH keydb net
169
170
171toxidSearch :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int))
172 -> TransportCrypto
173 -> Client r
174 -> Search NodeId (IP, PortNumber) Nonce32 NodeInfo Rendezvous
175toxidSearch getTimeout crypto client = Search
176 { searchSpace = toxSpace
177 , searchNodeAddress = nodeIP &&& nodePort
178 , searchQuery = getRendezvous getTimeout crypto client
179 }
180
181announceSerializer :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int))
182 -> MethodSerializer
183 TransactionId
184 (OnionDestination r)
185 (OnionMessage Identity)
186 PacketKind
187 AnnounceRequest
188 (Maybe AnnounceResponse)
189announceSerializer getTimeout = MethodSerializer
190 { methodTimeout = getTimeout
191 , method = AnnounceType
192 , wrapQuery = \(TransactionId n8 n24) src dst req ->
193 -- :: tid -> addr -> addr -> a -> OnionMessage Identity
194 OnionAnnounce $ Asymm
195 { -- The public key is our real long term public key if we want to
196 -- announce ourselves, a temporary one if we are searching for
197 -- friends.
198 senderKey = onionKey src
199 , asymmNonce = n24
200 , asymmData = Identity (req, n8)
201 }
202 , unwrapResponse = \case -- :: OnionMessage Identity -> b
203 OnionAnnounceResponse _ _ resp -> Just $ runIdentity resp
204 _ -> Nothing
205 }
206
207unwrapAnnounceResponse :: Maybe NodeId -> NodeInfo -> AnnounceResponse -> ([NodeInfo], [Rendezvous], Maybe Nonce32)
208unwrapAnnounceResponse alias ni (AnnounceResponse is_stored (SendNodes ns))
209 = case is_stored of
210 NotStored n32 -> ( ns , [] , Just n32)
211 SendBackKey k -> ( ns , [Rendezvous k ni] , Nothing )
212 Acknowledged n32 -> ( ns , maybeToList $ fmap (\k -> Rendezvous (id2key k) ni) alias , Just n32)
213
214-- TODO Announce key to announce peers.
215--
216-- Announce Peers are only put in the 8 closest peers array if they respond
217-- to an announce request. If the peers fail to respond to 3 announce
218-- requests they are deemed timed out and removed.
219--
220-- ...
221--
222-- For this reason, after the peer is announced successfully for 17 seconds,
223-- announce packets are sent aggressively every 3 seconds to each known close
224-- peer (in the list of 8 peers) to search aggressively for peers that know
225-- the peer we are searching for.
226
227-- TODO
228-- If toxcore goes offline (no onion traffic for 20 seconds) toxcore will
229-- aggressively reannounce itself and search for friends as if it was just
230-- started.
231
232
233sendOnion :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int))
234 -> Client r
235 -> AnnounceRequest
236 -> OnionDestination r
237 -> (NodeInfo -> AnnounceResponse -> t)
238 -> IO (Maybe t)
239sendOnion getTimeout client req oaddr unwrap =
240 -- Four tries and then we tap out.
241 flip fix 4 $ \loop n -> do
242 mb <- QR.sendQuery client (announceSerializer getTimeout) req oaddr
243 maybe (if n>0 then loop $! n - 1 else return Nothing)
244 (return . Just . unwrap (onionNodeInfo oaddr))
245 $ join mb
246
247-- | Lookup the secret counterpart for a given alias key.
248getRendezvous :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int))
249 -> TransportCrypto
250 -> Client r
251 -> NodeId
252 -> NodeInfo
253 -> IO (Maybe ([NodeInfo],[Rendezvous],Maybe Nonce32))
254getRendezvous getTimeout crypto client nid ni = do
255 asel <- atomically $ selectAlias crypto nid
256 let oaddr = OnionDestination asel ni Nothing
257 rkey = case asel of
258 SearchingAlias -> Nothing
259 _ -> Just $ key2id $ rendezvousPublic crypto
260 sendOnion getTimeout client
261 (AnnounceRequest zeros32 nid $ fromMaybe zeroID rkey)
262 oaddr
263 (unwrapAnnounceResponse rkey)
264
265putRendezvous :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int))
266 -> TransportCrypto
267 -> Client r
268 -> PublicKey
269 -> Nonce32
270 -> NodeInfo
271 -> IO (Maybe (Rendezvous, AnnounceResponse))
272putRendezvous getTimeout crypto client pubkey nonce32 ni = do
273 let longTermKey = key2id pubkey
274 rkey = rendezvousPublic crypto
275 rendezvousKey = key2id rkey
276 asel <- atomically $ selectAlias crypto longTermKey
277 let oaddr = OnionDestination asel ni Nothing
278 sendOnion getTimeout client (AnnounceRequest nonce32 longTermKey rendezvousKey) oaddr
279 $ \ni resp -> (Rendezvous rkey ni, resp)
diff --git a/src/Network/Tox/Onion/Transport.hs b/src/Network/Tox/Onion/Transport.hs
new file mode 100644
index 00000000..5c4544ca
--- /dev/null
+++ b/src/Network/Tox/Onion/Transport.hs
@@ -0,0 +1,927 @@
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE GADTs #-}
5{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6{-# LANGUAGE KindSignatures #-}
7{-# LANGUAGE LambdaCase #-}
8{-# LANGUAGE MultiParamTypeClasses #-}
9{-# LANGUAGE PartialTypeSignatures #-}
10{-# LANGUAGE RankNTypes #-}
11{-# LANGUAGE ScopedTypeVariables #-}
12{-# LANGUAGE StandaloneDeriving #-}
13{-# LANGUAGE TupleSections #-}
14{-# LANGUAGE TypeFamilies #-}
15{-# LANGUAGE TypeOperators #-}
16{-# LANGUAGE UndecidableInstances #-}
17module Network.Tox.Onion.Transport
18 ( parseOnionAddr
19 , encodeOnionAddr
20 , parseDataToRoute
21 , encodeDataToRoute
22 , forwardOnions
23 , AliasSelector(..)
24 , OnionDestination(..)
25 , OnionMessage(..)
26 , Rendezvous(..)
27 , DataToRoute(..)
28 , OnionData(..)
29 , AnnouncedRendezvous(..)
30 , AnnounceResponse(..)
31 , AnnounceRequest(..)
32 , Forwarding(..)
33 , ReturnPath(..)
34 , OnionRequest(..)
35 , OnionResponse(..)
36 , Addressed(..)
37 , UDPTransport
38 , KeyRecord(..)
39 , encrypt
40 , decrypt
41 , peelSymmetric
42 , OnionRoute(..)
43 , N3
44 , onionKey
45 , onionAliasSelector
46 , selectAlias
47 , RouteId(..)
48 , routeId
49 ) where
50
51import Network.Address (fromSockAddr,toSockAddr,setPort,either4or6,sockAddrPort)
52import Network.QueryResponse
53import Crypto.Tox hiding (encrypt,decrypt)
54import Network.Tox.NodeId
55import qualified Crypto.Tox as ToxCrypto
56import Network.Tox.DHT.Transport (NodeInfo(..),NodeId(..),SendNodes(..),nodeInfo,DHTPublicKey(..),FriendRequest,asymNodeInfo)
57
58import Control.Applicative
59import Control.Arrow
60import Control.Concurrent.STM
61import Control.Monad
62import qualified Data.ByteString as B
63 ;import Data.ByteString (ByteString)
64import Data.Coerce
65import Data.Function
66import Data.Functor.Contravariant
67import Data.Functor.Identity
68import Data.IP
69import Data.Maybe
70import Data.Monoid
71import Data.Serialize as S
72import Data.Type.Equality
73import Data.Typeable
74import Data.Word
75import Debug.Trace
76import GHC.Generics ()
77import GHC.TypeLits
78import Network.Socket
79import System.IO
80import qualified Text.ParserCombinators.ReadP as RP
81import Data.Hashable
82
83type HandleLo a = Maybe (Either String (ByteString, SockAddr)) -> IO a
84
85type UDPTransport = Transport String SockAddr ByteString
86
87
88getOnionAsymm :: Get (Asymm (Encrypted DataToRoute))
89getOnionAsymm = getAliasedAsymm
90
91putOnionAsymm :: Serialize a => Word8 -> Put -> Asymm a -> Put
92putOnionAsymm typ p a = put typ >> p >> putAliasedAsymm a
93
94data OnionMessage (f :: * -> *)
95 = OnionAnnounce (Asymm (f (AnnounceRequest,Nonce8)))
96 | OnionAnnounceResponse Nonce8 Nonce24 (f AnnounceResponse)
97 | OnionToRoute PublicKey (Asymm (Encrypted DataToRoute)) -- destination key, aliased Asymm
98 | OnionToRouteResponse (Asymm (Encrypted DataToRoute))
99
100deriving instance ( Show (f (AnnounceRequest, Nonce8))
101 , Show (f AnnounceResponse)
102 , Show (f DataToRoute)
103 ) => Show (OnionMessage f)
104
105msgNonce :: OnionMessage f -> Nonce24
106msgNonce (OnionAnnounce a) = asymmNonce a
107msgNonce (OnionAnnounceResponse _ n24 _) = n24
108msgNonce (OnionToRoute _ a) = asymmNonce a
109msgNonce (OnionToRouteResponse a) = asymmNonce a
110
111data AliasSelector = SearchingAlias | AnnouncingAlias SecretKey PublicKey
112 deriving (Eq,Show)
113
114data OnionDestination r
115 = OnionToOwner
116 { onionNodeInfo :: NodeInfo
117 , onionReturnPath :: ReturnPath N3 -- ^ Somebody else's path to us.
118 }
119 | OnionDestination
120 { onionAliasSelector' :: AliasSelector
121 , onionNodeInfo :: NodeInfo
122 , onionRouteSpec :: Maybe r -- ^ Our own onion-path.
123 }
124 deriving Show
125
126onionAliasSelector :: OnionDestination r -> AliasSelector
127onionAliasSelector (OnionToOwner {} ) = SearchingAlias
128onionAliasSelector (OnionDestination{onionAliasSelector' = sel}) = sel
129
130onionKey :: OnionDestination r -> PublicKey
131onionKey od = id2key . nodeId $ onionNodeInfo od
132
133instance Sized (OnionMessage Encrypted) where
134 size = VarSize $ \case
135 OnionAnnounce a -> case size of ConstSize n -> n + 1
136 VarSize f -> f a + 1
137 OnionAnnounceResponse n8 n24 x -> case size of ConstSize n -> n + 33
138 VarSize f -> f x + 33
139 OnionToRoute pubkey a -> case size of ConstSize n -> n + 33
140 VarSize f -> f a + 33
141 OnionToRouteResponse a -> case size of ConstSize n -> n + 1
142 VarSize f -> f a + 1
143
144instance Serialize (OnionMessage Encrypted) where
145 get = do
146 typ <- get
147 case typ :: Word8 of
148 0x83 -> OnionAnnounce <$> getAliasedAsymm
149 0x85 -> OnionToRoute <$> getPublicKey <*> getAliasedAsymm
150 t -> fail ("Unknown onion payload: " ++ show t)
151 `fromMaybe` getOnionReply t
152 put (OnionAnnounce a) = putWord8 0x83 >> putAliasedAsymm a
153 put (OnionToRoute k a) = putWord8 0x85 >> putPublicKey k >> putAliasedAsymm a
154 put (OnionAnnounceResponse n8 n24 x) = putWord8 0x84 >> put n8 >> put n24 >> put x
155 put (OnionToRouteResponse a) = putWord8 0x86 >> putAliasedAsymm a
156
157onionToOwner :: Asymm a -> ReturnPath N3 -> SockAddr -> Either String (OnionDestination r)
158onionToOwner asymm ret3 saddr = do
159 ni <- nodeInfo (key2id $ senderKey asymm) saddr
160 return $ OnionToOwner ni ret3
161-- data CookieAddress = WithoutCookie NodeInfo | CookieAddress Cookie SockAddr
162
163
164onion :: Sized msg =>
165 ByteString
166 -> SockAddr
167 -> Get (Asymm (Encrypted msg) -> t)
168 -> Either String (t, OnionDestination r)
169onion bs saddr getf = do (f,(asymm,ret3)) <- runGet ((,) <$> getf <*> getOnionRequest) bs
170 oaddr <- onionToOwner asymm ret3 saddr
171 return (f asymm, oaddr)
172
173parseOnionAddr :: (SockAddr -> Nonce8 -> IO (Maybe (OnionDestination r)))
174 -> (ByteString, SockAddr)
175 -> IO (Either (OnionMessage Encrypted,OnionDestination r)
176 (ByteString,SockAddr))
177parseOnionAddr lookupSender (msg,saddr)
178 | Just (typ,bs) <- B.uncons msg
179 , let right = Right (msg,saddr)
180 query = return . either (const right) Left
181 = case typ of
182 0x83 -> query $ onion bs saddr (pure OnionAnnounce) -- Announce Request
183 0x85 -> query $ onion bs saddr (OnionToRoute <$> getPublicKey) -- Onion Data Request
184 _ -> case flip runGet bs <$> getOnionReply typ of
185 Just (Right msg@(OnionAnnounceResponse n8 _ _)) -> do
186 maddr <- lookupSender saddr n8
187 maybe (return right) -- Response unsolicited or too late.
188 (return . Left . \od -> (msg,od))
189 maddr
190 Just (Right msg@(OnionToRouteResponse asym)) -> do
191 let ni = asymNodeInfo saddr asym
192 return $ Left (msg, OnionDestination SearchingAlias ni Nothing)
193 _ -> return right
194
195getOnionReply :: Word8 -> Maybe (Get (OnionMessage Encrypted))
196getOnionReply 0x84 = Just $ OnionAnnounceResponse <$> get <*> get <*> get
197getOnionReply 0x86 = Just $ OnionToRouteResponse <$> getOnionAsymm
198getOnionReply _ = Nothing
199
200putOnionMsg :: OnionMessage Encrypted -> Put
201putOnionMsg (OnionAnnounce a) = putOnionAsymm 0x83 (return ()) a
202putOnionMsg (OnionToRoute pubkey a) = putOnionAsymm 0x85 (putPublicKey pubkey) a
203putOnionMsg (OnionAnnounceResponse n8 n24 x) = put (0x84 :: Word8) >> put n8 >> put n24 >> put x
204putOnionMsg (OnionToRouteResponse a) = putOnionAsymm 0x86 (return ()) a
205
206newtype RouteId = RouteId Int
207 deriving Show
208
209
210-- We used to derive the RouteId from the Nonce8 associated with the query.
211-- This is problematic because a nonce generated by toxcore will not validate
212-- if it is received via a different route than it was issued. This is
213-- described by the Tox spec:
214--
215-- Toxcore generates `ping_id`s by taking a 32 byte sha hash of the current
216-- time, some secret bytes generated when the instance is created, the
217-- current time divided by a 20 second timeout, the public key of the
218-- requester and the source ip/port that the packet was received from. Since
219-- the ip/port that the packet was received from is in the `ping_id`, the
220-- announce packets being sent with a ping id must be sent using the same
221-- path as the packet that we received the `ping_id` from or announcing will
222-- fail.
223--
224-- The original idea was:
225--
226-- > routeId :: Nonce8 -> RouteId
227-- > routeId (Nonce8 w8) = RouteId $ mod (fromIntegral w8) 12
228--
229-- Instead, we'll just hash the destination node id.
230routeId :: NodeId -> RouteId
231routeId nid = RouteId $ mod (hash nid) 12
232
233
234encodeOnionAddr :: TransportCrypto
235 -> (NodeInfo -> RouteId -> IO (Maybe OnionRoute))
236 -> (OnionMessage Encrypted,OnionDestination RouteId)
237 -> IO (Maybe (ByteString, SockAddr))
238encodeOnionAddr crypto _ (msg,OnionToOwner ni p) =
239 return $ Just ( runPut $ putResponse (OnionResponse p msg)
240 , nodeAddr ni )
241encodeOnionAddr crypto getRoute (msg,OnionDestination x ni Nothing) = do
242 encodeOnionAddr crypto getRoute (msg,OnionDestination x ni (Just $ routeId $ nodeId ni) )
243 -- hPutStrLn stderr $ "ONION encode missing routeid"
244 -- return Nothing
245encodeOnionAddr crypto getRoute (msg,OnionDestination _ ni (Just rid)) = do
246 let go route = do
247 req <- wrapForRoute crypto msg ni route
248 return ( runPut $ putRequest req
249 , nodeAddr $ routeNodeA route)
250 mapM' f x = do
251 let _ = x :: Maybe OnionRoute
252 -- hPutStrLn stderr $ "ONION encode sending to " ++ show ni
253 -- hPutStrLn stderr $ "ONION encode getRoute -> " ++ show (fmap (\y -> map ($ y) [routeNodeA,routeNodeB,routeNodeC]) x)
254 mapM f x -- ONION encode getRoute -> Nothing
255 getRoute ni rid >>= mapM' go
256
257
258forwardOnions :: TransportCrypto -> UDPTransport -> UDPTransport
259forwardOnions crypto udp = udp { awaitMessage = forwardAwait crypto udp }
260
261forwardAwait :: TransportCrypto -> UDPTransport -> HandleLo a -> IO a
262forwardAwait crypto udp kont = do
263 fix $ \another -> do
264 awaitMessage udp $ \case
265 m@(Just (Right (bs,saddr))) -> case B.head bs of
266 0x80 -> forward kont bs $ handleOnionRequest (Proxy :: Proxy N0) crypto saddr udp another
267 0x81 -> forward kont bs $ handleOnionRequest (Proxy :: Proxy N1) crypto saddr udp another
268 0x82 -> forward kont bs $ handleOnionRequest (Proxy :: Proxy N2) crypto saddr udp another
269 0x8c -> forward kont bs $ handleOnionResponse (Proxy :: Proxy N3) crypto saddr udp another
270 0x8d -> forward kont bs $ handleOnionResponse (Proxy :: Proxy N2) crypto saddr udp another
271 0x8e -> forward kont bs $ handleOnionResponse (Proxy :: Proxy N1) crypto saddr udp another
272 _ -> kont m
273 m -> kont m
274
275forward :: forall c b b1. (Serialize b, Show b) =>
276 (Maybe (Either String b1) -> c) -> ByteString -> (b -> c) -> c
277forward kont bs f = either (kont . Just . Left) f $ decode $ B.tail bs
278
279class SumToThree a b
280
281instance SumToThree N0 N3
282instance SumToThree (S a) b => SumToThree a (S b)
283
284class ( Serialize (ReturnPath n)
285 , Serialize (ReturnPath (S n))
286 , Serialize (Forwarding (ThreeMinus (S n)) (OnionMessage Encrypted))
287 , ThreeMinus n ~ S (ThreeMinus (S n))
288 ) => LessThanThree n
289
290instance LessThanThree N0
291instance LessThanThree N1
292instance LessThanThree N2
293
294type family ThreeMinus n where
295 ThreeMinus N3 = N0
296 ThreeMinus N2 = N1
297 ThreeMinus N1 = N2
298 ThreeMinus N0 = N3
299
300-- n = 0, 1, 2
301data OnionRequest n = OnionRequest
302 { onionNonce :: Nonce24
303 , onionForward :: Forwarding (ThreeMinus n) (OnionMessage Encrypted)
304 , pathFromOwner :: ReturnPath n
305 }
306
307deriving instance ( Show (Forwarding (ThreeMinus n) (OnionMessage Encrypted))
308 , KnownNat (PeanoNat n)
309 ) => Show (OnionRequest n)
310
311instance ( Serialize (Forwarding (ThreeMinus n) (OnionMessage Encrypted))
312 , Sized (ReturnPath n)
313 , Serialize (ReturnPath n)
314 , Typeable n
315 ) => Serialize (OnionRequest n) where
316 get = do
317 -- TODO share code with 'getOnionRequest'
318 n24 <- case eqT :: Maybe (n :~: N3) of
319 Just Refl -> return $ Nonce24 zeros24
320 Nothing -> get
321 cnt <- remaining
322 let fwdsize = case size :: Size (ReturnPath n) of ConstSize n -> cnt - n
323 fwd <- isolate fwdsize get
324 rpath <- get
325 return $ OnionRequest n24 fwd rpath
326 put (OnionRequest n f p) = maybe (put n) (\Refl -> return ()) (eqT :: Maybe (n :~: N3)) >> put f >> put p
327
328-- getRequest :: _
329-- getRequest = OnionRequest <$> get <*> get <*> get
330
331-- n = 1, 2, 3
332-- Attributed (Encrypted (
333
334data OnionResponse n = OnionResponse
335 { pathToOwner :: ReturnPath n
336 , msgToOwner :: OnionMessage Encrypted
337 }
338
339deriving instance KnownNat (PeanoNat n) => Show (OnionResponse n)
340
341instance ( Serialize (ReturnPath n) ) => Serialize (OnionResponse n) where
342 get = OnionResponse <$> get <*> (get >>= fromMaybe (fail "illegal onion forwarding")
343 . getOnionReply)
344 put (OnionResponse p m) = put p >> putOnionMsg m
345
346
347data Addressed a = Addressed { sockAddr :: SockAddr, unaddressed :: a }
348 deriving (Eq,Show)
349
350instance Sized a => Sized (Addressed a) where
351 size = case size :: Size a of
352 ConstSize n -> ConstSize $ 1{-family-} + 16{-ip-} + 2{-port-} + n
353 VarSize f -> VarSize $ \x -> 1{-family-} + 16{-ip-} + 2{-port-} + f (unaddressed x)
354
355getForwardAddr :: S.Get SockAddr
356getForwardAddr = do
357 addrfam <- S.get :: S.Get Word8
358 ip <- getIP addrfam
359 case ip of IPv4 _ -> S.skip 12 -- compliant peers would zero-fill this.
360 IPv6 _ -> return ()
361 port <- S.get :: S.Get PortNumber
362 return $ setPort port $ toSockAddr ip
363
364
365putForwardAddr :: SockAddr -> S.Put
366putForwardAddr saddr = fromMaybe (return $ error "unsupported SockAddr family") $ do
367 port <- sockAddrPort saddr
368 ip <- fromSockAddr $ either id id $ either4or6 saddr
369 return $ do
370 case ip of
371 IPv4 ip4 -> S.put (0x02 :: Word8) >> S.put ip4 >> S.putByteString (B.replicate 12 0)
372 IPv6 ip6 -> S.put (0x0a :: Word8) >> S.put ip6
373 S.put port
374
375instance Serialize a => Serialize (Addressed a) where
376 get = Addressed <$> getForwardAddr <*> get
377 put (Addressed addr x) = putForwardAddr addr >> put x
378
379data N0
380data S n
381type N1 = S N0
382type N2 = S N1
383type N3 = S N2
384
385class KnownPeanoNat n where
386 peanoVal :: p n -> Int
387
388instance KnownPeanoNat N0 where
389 peanoVal _ = 0
390instance KnownPeanoNat n => KnownPeanoNat (S n) where
391 peanoVal _ = 1 + peanoVal (Proxy :: Proxy n)
392
393type family PeanoNat p where
394 PeanoNat N0 = 0
395 PeanoNat (S n) = 1 + PeanoNat n
396
397data ReturnPath n where
398 NoReturnPath :: ReturnPath N0
399 ReturnPath :: Nonce24 -> Encrypted (Addressed (ReturnPath n)) -> ReturnPath (S n)
400
401-- Size: 59 = 1(family) + 16(ip) + 2(port) +16(mac) + 24(nonce)
402instance Sized (ReturnPath N0) where size = ConstSize 0
403instance Sized (ReturnPath n) => Sized (ReturnPath (S n)) where
404 size = ConstSize 59 <> contramap (\x -> let _ = x :: ReturnPath (S n)
405 in error "non-constant ReturnPath size")
406 (size :: Size (ReturnPath n))
407
408{-
409instance KnownNat (PeanoNat n) => Sized (ReturnPath n) where
410 size = ConstSize $ 59 * fromIntegral (natVal (Proxy :: Proxy (PeanoNat n)))
411-}
412
413instance Serialize (ReturnPath N0) where get = pure NoReturnPath
414 put NoReturnPath = pure ()
415
416instance Serialize (ReturnPath N1) where
417 get = ReturnPath <$> get <*> get
418 put (ReturnPath n24 p) = put n24 >> put p
419
420instance (Sized (ReturnPath n), Serialize (ReturnPath n)) => Serialize (ReturnPath (S (S n))) where
421 get = ReturnPath <$> get <*> get
422 put (ReturnPath n24 p) = put n24 >> put p
423
424
425{-
426-- This doesn't work because it tried to infer it for (0 - 1)
427instance (Serialize (Encrypted (Addressed (ReturnPath (n - 1))))) => Serialize (ReturnPath n) where
428 get = ReturnPath <$> get <*> get
429 put (ReturnPath n24 p) = put n24 >> put p
430-}
431
432instance KnownNat (PeanoNat n) => Show (ReturnPath n) where
433 show rpath = "ReturnPath" ++ show (natVal (Proxy :: Proxy (PeanoNat n)))
434
435
436-- instance KnownNat n => Serialize (ReturnPath n) where
437-- -- Size: 59 = 1(family) + 16(ip) + 2(port) +16(mac) + 24(nonce)
438-- get = ReturnPath <$> getBytes ( 59 * (fromIntegral $ natVal $ Proxy @n) )
439-- put (ReturnPath bs) = putByteString bs
440
441
442data Forwarding n msg where
443 NotForwarded :: msg -> Forwarding N0 msg
444 Forwarding :: PublicKey -> Encrypted (Addressed (Forwarding n msg)) -> Forwarding (S n) msg
445
446instance Show msg => Show (Forwarding N0 msg) where
447 show (NotForwarded x) = "NotForwarded "++show x
448
449instance ( KnownNat (PeanoNat (S n))
450 , Show (Encrypted (Addressed (Forwarding n msg)))
451 ) => Show (Forwarding (S n) msg) where
452 show (Forwarding k a) = unwords [ "Forwarding"
453 , "("++show (natVal (Proxy :: Proxy (PeanoNat (S n))))++")"
454 , show (key2id k)
455 , show a
456 ]
457
458instance Sized msg => Sized (Forwarding N0 msg)
459 where size = case size :: Size msg of
460 ConstSize n -> ConstSize n
461 VarSize f -> VarSize $ \(NotForwarded x) -> f x
462
463instance Sized (Forwarding n msg) => Sized (Forwarding (S n) msg)
464 where size = ConstSize 32
465 <> contramap (\(Forwarding _ e) -> e)
466 (size :: Size (Encrypted (Addressed (Forwarding n msg))))
467
468instance Serialize msg => Serialize (Forwarding N0 msg) where
469 get = NotForwarded <$> get
470 put (NotForwarded msg) = put msg
471
472instance (Serialize (Encrypted (Addressed (Forwarding n msg)))) => Serialize (Forwarding (S n) msg) where
473 get = Forwarding <$> getPublicKey <*> get
474 put (Forwarding k x) = putPublicKey k >> put x
475
476handleOnionRequest :: forall a proxy n.
477 ( LessThanThree n
478 , KnownPeanoNat n
479 , Sized (ReturnPath n)
480 , Typeable n
481 ) => proxy n -> TransportCrypto -> SockAddr -> UDPTransport -> IO a -> OnionRequest n -> IO a
482handleOnionRequest proxy crypto saddr udp kont (OnionRequest nonce msg rpath) = do
483 let n = peanoVal rpath
484 hPutStrLn stderr $ "handleOnionRequest " ++ show n
485 (sym, snonce) <- atomically ( (,) <$> transportSymmetric crypto
486 <*> transportNewNonce crypto )
487 peeled <- peelOnion crypto nonce msg
488 case peeled of
489 Left e -> do
490 -- todo report encryption error
491 hPutStrLn stderr $ unwords [ "peelOnion:", show n, either show show (either4or6 saddr), e]
492 kont
493 Right (Addressed dst msg') -> do
494 hPutStrLn stderr $ unwords [ "peelOnion:", show n, either show show (either4or6 saddr), "-->", either show show (either4or6 dst), "SUCCESS"]
495 sendMessage udp dst (runPut $ putRequest $ OnionRequest nonce msg' $ wrapSymmetric sym snonce saddr rpath)
496 kont
497
498wrapSymmetric :: Serialize (ReturnPath n) =>
499 SymmetricKey -> Nonce24 -> SockAddr -> ReturnPath n -> ReturnPath (S n)
500wrapSymmetric sym n saddr rpath = ReturnPath n $ encryptSymmetric sym n (encodePlain $ Addressed saddr rpath)
501
502peelSymmetric :: Serialize (Addressed (ReturnPath n))
503 => SymmetricKey -> ReturnPath (S n) -> Either String (Addressed (ReturnPath n))
504peelSymmetric sym (ReturnPath nonce e) = decryptSymmetric sym nonce e >>= decodePlain
505
506
507peelOnion :: Serialize (Addressed (Forwarding n t))
508 => TransportCrypto
509 -> Nonce24
510 -> Forwarding (S n) t
511 -> IO (Either String (Addressed (Forwarding n t)))
512peelOnion crypto nonce (Forwarding k fwd) = do
513 fmap runIdentity . uncomposed <$> decryptMessage crypto (dhtKey crypto) nonce (Right $ Asymm k nonce fwd)
514
515handleOnionResponse :: (KnownPeanoNat n, Sized (ReturnPath n), Serialize (ReturnPath n)) => proxy (S n) -> TransportCrypto -> SockAddr -> UDPTransport -> IO a -> OnionResponse (S n) -> IO a
516handleOnionResponse proxy crypto saddr udp kont (OnionResponse path msg) = do
517 sym <- atomically $ transportSymmetric crypto
518 case peelSymmetric sym path of
519 Left e -> do
520 -- todo report encryption error
521 let n = peanoVal path
522 hPutStrLn stderr $ unwords [ "peelSymmetric:", show n, either show show (either4or6 saddr), e]
523 kont
524 Right (Addressed dst path') -> do
525 sendMessage udp dst (runPut $ putResponse $ OnionResponse path' msg)
526 kont
527
528
529data AnnounceRequest = AnnounceRequest
530 { announcePingId :: Nonce32 -- Ping ID
531 , announceSeeking :: NodeId -- Public key we are searching for
532 , announceKey :: NodeId -- Public key that we want those sending back data packets to use
533 }
534 deriving Show
535
536instance Sized AnnounceRequest where size = ConstSize (32*3)
537
538instance S.Serialize AnnounceRequest where
539 get = AnnounceRequest <$> S.get <*> S.get <*> S.get
540 put (AnnounceRequest p s k) = S.put (p,s,k)
541
542getOnionRequest :: Sized msg => Get (Asymm (Encrypted msg), ReturnPath N3)
543getOnionRequest = do
544 -- Assumes return path is constant size so that we can isolate
545 -- the variable-sized prefix.
546 cnt <- remaining
547 a <- isolate (case size :: Size (ReturnPath N3) of ConstSize n -> cnt - n)
548 getAliasedAsymm
549 path <- get
550 return (a,path)
551
552putRequest :: ( KnownPeanoNat n
553 , Serialize (OnionRequest n)
554 , Typeable n
555 ) => OnionRequest n -> Put
556putRequest req = do
557 let tag = 0x80 + fromIntegral (peanoVal req)
558 when (tag <= 0x82) (putWord8 tag)
559 put req
560
561putResponse :: (KnownPeanoNat n, Serialize (OnionResponse n)) => OnionResponse n -> Put
562putResponse resp = do
563 let tag = 0x8f - fromIntegral (peanoVal resp)
564 -- OnionResponse N0 is an alias for the OnionMessage Encrypted type which includes a tag
565 -- in it's Serialize instance.
566 when (tag /= 0x8f) (putWord8 tag)
567 put resp
568
569
570data KeyRecord = NotStored Nonce32
571 | SendBackKey PublicKey
572 | Acknowledged Nonce32
573 deriving Show
574
575instance Sized KeyRecord where size = ConstSize 33
576
577instance S.Serialize KeyRecord where
578 get = do
579 is_stored <- S.get :: S.Get Word8
580 case is_stored of
581 1 -> SendBackKey <$> getPublicKey
582 2 -> Acknowledged <$> S.get
583 _ -> NotStored <$> S.get
584 put (NotStored n32) = S.put (0 :: Word8) >> S.put n32
585 put (SendBackKey key) = S.put (1 :: Word8) >> putPublicKey key
586 put (Acknowledged n32) = S.put (2 :: Word8) >> S.put n32
587
588data AnnounceResponse = AnnounceResponse
589 { is_stored :: KeyRecord
590 , announceNodes :: SendNodes
591 }
592 deriving Show
593
594instance Sized AnnounceResponse where
595 size = contramap is_stored size <> contramap announceNodes size
596
597getNodeList :: S.Get [NodeInfo]
598getNodeList = do
599 n <- S.get
600 (:) n <$> (getNodeList <|> pure [])
601
602instance S.Serialize AnnounceResponse where
603 get = AnnounceResponse <$> S.get <*> (SendNodes <$> getNodeList)
604 put (AnnounceResponse st (SendNodes ns)) = S.put st >> mapM_ S.put ns
605
606data DataToRoute = DataToRoute
607 { dataFromKey :: PublicKey -- Real public key of sender
608 , dataToRoute :: Encrypted OnionData -- (Word8,ByteString) -- DHTPK 0x9c
609 }
610
611instance Sized DataToRoute where
612 size = ConstSize 32 <> contramap dataToRoute size
613
614instance Serialize DataToRoute where
615 get = DataToRoute <$> getPublicKey <*> get
616 put (DataToRoute k dta) = putPublicKey k >> put dta
617
618data OnionData
619 = -- | type 0x9c
620 --
621 -- We send this packet every 30 seconds if there is more than one peer (in
622 -- the 8) that says they our friend is announced on them. This packet can
623 -- also be sent through the DHT module as a DHT request packet (see DHT) if
624 -- we know the DHT public key of the friend and are looking for them in the
625 -- DHT but have not connected to them yet. 30 second is a reasonable
626 -- timeout to not flood the network with too many packets while making sure
627 -- the other will eventually receive the packet. Since packets are sent
628 -- through every peer that knows the friend, resending it right away
629 -- without waiting has a high likelihood of failure as the chances of
630 -- packet loss happening to all (up to to 8) packets sent is low.
631 --
632 -- If a friend is online and connected to us, the onion will stop all of
633 -- its actions for that friend. If the peer goes offline it will restart
634 -- searching for the friend as if toxcore was just started.
635 OnionDHTPublicKey DHTPublicKey
636 | -- | type 0x20
637 --
638 --
639 OnionFriendRequest FriendRequest -- 0x20
640 deriving (Eq,Show)
641
642instance Sized OnionData where
643 size = VarSize $ \case
644 OnionDHTPublicKey dhtpk -> case size of
645 ConstSize n -> n -- Override because OnionData probably
646 -- should be treated as variable sized.
647 VarSize f -> f dhtpk
648 -- FIXME: inconsitantly, we have to add in the tag byte for this case.
649 OnionFriendRequest req -> 1 + case size of
650 ConstSize n -> n
651 VarSize f -> f req
652
653instance Serialize OnionData where
654 get = do
655 tag <- get
656 case tag :: Word8 of
657 0x9c -> OnionDHTPublicKey <$> get
658 0x20 -> OnionFriendRequest <$> get
659 _ -> fail $ "Unknown onion data: "++show tag
660 put (OnionDHTPublicKey dpk) = put (0x9c :: Word8) >> put dpk
661 put (OnionFriendRequest fr) = put (0x20 :: Word8) >> put fr
662
663selectKey :: TransportCrypto -> OnionMessage f -> OnionDestination r -> IO (SecretKey, PublicKey)
664selectKey crypto _ rpath@(OnionDestination (AnnouncingAlias skey pkey) _ _)
665 = return (skey, pkey)
666selectKey crypto msg rpath = return $ aliasKey crypto rpath
667
668encrypt :: TransportCrypto
669 -> OnionMessage Identity
670 -> OnionDestination r
671 -> IO (OnionMessage Encrypted, OnionDestination r)
672encrypt crypto msg rpath = do
673 (skey,pkey) <- selectKey crypto msg rpath -- source key
674 let okey = onionKey rpath -- destination key
675 encipher1 :: Serialize a => SecretKey -> PublicKey -> Nonce24 -> a -> (IO ∘ Encrypted) a
676 encipher1 sk pk n a = Composed $ do
677 secret <- lookupSharedSecret crypto sk pk n
678 return $ ToxCrypto.encrypt secret $ encodePlain a
679 encipher :: Serialize a => Nonce24 -> Either (Identity a) (Asymm (Identity a)) -> (IO ∘ Encrypted) a
680 encipher n d = encipher1 skey okey n $ either runIdentity (runIdentity . asymmData) d
681 m <- sequenceMessage $ transcode encipher msg
682 return (m, rpath)
683
684decrypt :: TransportCrypto -> OnionMessage Encrypted -> OnionDestination r -> IO (Either String (OnionMessage Identity, OnionDestination r))
685decrypt crypto msg addr = do
686 (skey,pkey) <- selectKey crypto msg addr
687 let decipher1 :: Serialize a =>
688 TransportCrypto -> SecretKey -> Nonce24
689 -> Either (PublicKey,Encrypted a) (Asymm (Encrypted a))
690 -> (IO ∘ Either String ∘ Identity) a
691 decipher1 crypto k n arg = Composed $ do
692 let (sender,e) = either id (senderKey &&& asymmData) arg
693 secret <- lookupSharedSecret crypto k sender n
694 return $ Composed $ do
695 plain <- ToxCrypto.decrypt secret e
696 Identity <$> decodePlain plain
697 decipher :: Serialize a
698 => Nonce24 -> Either (Encrypted a) (Asymm (Encrypted a))
699 -> (IO ∘ Either String ∘ Identity) a
700 decipher = (\n -> decipher1 crypto skey n . left (senderkey addr))
701 foo <- sequenceMessage $ transcode decipher msg
702 return $ do
703 msg <- sequenceMessage foo
704 Right (msg, addr)
705
706senderkey :: OnionDestination r -> t -> (PublicKey, t)
707senderkey addr e = (onionKey addr, e)
708
709aliasKey :: TransportCrypto -> OnionDestination r -> (SecretKey,PublicKey)
710aliasKey crypto (OnionToOwner {}) = (transportSecret &&& transportPublic) crypto
711aliasKey crypto (OnionDestination {}) = (onionAliasSecret &&& onionAliasPublic) crypto
712
713dhtKey :: TransportCrypto -> (SecretKey,PublicKey)
714dhtKey crypto = (transportSecret &&& transportPublic) crypto
715
716decryptMessage :: Serialize x =>
717 TransportCrypto
718 -> (SecretKey,PublicKey)
719 -> Nonce24
720 -> Either (PublicKey, Encrypted x)
721 (Asymm (Encrypted x))
722 -> IO ((Either String ∘ Identity) x)
723decryptMessage crypto (sk,pk) n arg = do
724 let (sender,e) = either id (senderKey &&& asymmData) arg
725 plain = Composed . fmap Identity . (>>= decodePlain)
726 secret <- lookupSharedSecret crypto sk sender n
727 return $ plain $ ToxCrypto.decrypt secret e
728
729sequenceMessage :: Applicative m => OnionMessage (m ∘ f) -> m (OnionMessage f)
730sequenceMessage (OnionAnnounce a) = fmap OnionAnnounce $ sequenceA $ fmap uncomposed a
731sequenceMessage (OnionAnnounceResponse n8 n24 dta) = OnionAnnounceResponse n8 n24 <$> uncomposed dta
732sequenceMessage (OnionToRoute pub a) = pure $ OnionToRoute pub a
733sequenceMessage (OnionToRouteResponse a) = pure $ OnionToRouteResponse a
734-- sequenceMessage (OnionToRouteResponse a) = fmap OnionToRouteResponse $ sequenceA $ fmap uncomposed a
735
736transcode :: forall f g. (forall a. Serialize a => Nonce24 -> Either (f a) (Asymm (f a)) -> g a) -> OnionMessage f -> OnionMessage g
737transcode f (OnionAnnounce a) = OnionAnnounce $ a { asymmData = f (asymmNonce a) (Right a) }
738transcode f (OnionAnnounceResponse n8 n24 dta) = OnionAnnounceResponse n8 n24 $ f n24 $ Left dta
739transcode f (OnionToRoute pub a) = OnionToRoute pub a
740transcode f (OnionToRouteResponse a) = OnionToRouteResponse a
741-- transcode f (OnionToRouteResponse a) = OnionToRouteResponse $ a { asymmData = f (asymmNonce a) (Right a) }
742
743
744data OnionRoute = OnionRoute
745 { routeAliasA :: SecretKey
746 , routeAliasB :: SecretKey
747 , routeAliasC :: SecretKey
748 , routeNodeA :: NodeInfo
749 , routeNodeB :: NodeInfo
750 , routeNodeC :: NodeInfo
751 }
752
753wrapForRoute :: TransportCrypto -> OnionMessage Encrypted -> NodeInfo -> OnionRoute -> IO (OnionRequest N0)
754wrapForRoute crypto msg ni r = do
755 -- We needn't use the same nonce value here, but I think it is safe to do so.
756 let nonce = msgNonce msg
757 fwd <- wrapOnion crypto (routeAliasA r)
758 nonce
759 (id2key . nodeId $ routeNodeA r)
760 (nodeAddr $ routeNodeB r)
761 =<< wrapOnion crypto (routeAliasB r)
762 nonce
763 (id2key . nodeId $ routeNodeB r)
764 (nodeAddr $ routeNodeC r)
765 =<< wrapOnion crypto (routeAliasC r)
766 nonce
767 (id2key . nodeId $ routeNodeC r)
768 (nodeAddr ni)
769 (NotForwarded msg)
770 return OnionRequest
771 { onionNonce = nonce
772 , onionForward = fwd
773 , pathFromOwner = NoReturnPath
774 }
775
776wrapOnion :: Serialize (Forwarding n msg) =>
777 TransportCrypto
778 -> SecretKey
779 -> Nonce24
780 -> PublicKey
781 -> SockAddr
782 -> Forwarding n msg
783 -> IO (Forwarding (S n) msg)
784wrapOnion crypto skey nonce destkey saddr fwd = do
785 let plain = encodePlain $ Addressed saddr fwd
786 secret <- lookupSharedSecret crypto skey destkey nonce
787 return $ Forwarding (toPublic skey) $ ToxCrypto.encrypt secret plain
788
789
790-- TODO
791-- Two types of packets may be sent to Rendezvous via OnionToRoute requests.
792--
793-- (1) DHT public key packet (0x9c)
794--
795-- (2) Friend request
796data Rendezvous = Rendezvous
797 { rendezvousKey :: PublicKey
798 , rendezvousNode :: NodeInfo
799 }
800 deriving Eq
801
802instance Show Rendezvous where
803 showsPrec d (Rendezvous k ni)
804 = showsPrec d (key2id k)
805 . (':' :)
806 . showsPrec d ni
807
808data AnnouncedRendezvous = AnnouncedRendezvous
809 { remoteUserKey :: PublicKey
810 , rendezvous :: Rendezvous
811 }
812 deriving Eq
813
814instance Show AnnouncedRendezvous where
815 showsPrec d (AnnouncedRendezvous remote rendez)
816 = showsPrec d (key2id remote)
817 . (':' :)
818 . showsPrec d rendez
819
820instance Read AnnouncedRendezvous where
821 readsPrec d = RP.readP_to_S $ do
822 ukstr <- RP.munch (/=':')
823 RP.char ':'
824 rkstr <- RP.munch (/=':')
825 RP.char ':'
826 nistr <- RP.munch (const True)
827 return AnnouncedRendezvous
828 { remoteUserKey = id2key $ read ukstr
829 , rendezvous = Rendezvous
830 { rendezvousKey = id2key $ read rkstr
831 , rendezvousNode = read nistr
832 }
833 }
834
835
836selectAlias :: TransportCrypto -> NodeId -> STM AliasSelector
837selectAlias crypto pkey = do
838 ks <- filter (\(sk,pk) -> pk == id2key pkey)
839 <$> readTVar (userKeys crypto)
840 maybe (return SearchingAlias)
841 (return . uncurry AnnouncingAlias)
842 (listToMaybe ks)
843
844
845parseDataToRoute
846 :: TransportCrypto
847 -> (OnionMessage Encrypted,OnionDestination r)
848 -> IO (Either ((PublicKey,OnionData),AnnouncedRendezvous) (OnionMessage Encrypted, OnionDestination r))
849parseDataToRoute crypto (OnionToRouteResponse dta, od) = do
850 ks <- atomically $ readTVar $ userKeys crypto
851
852 omsg0 <- decryptMessage crypto (rendezvousSecret crypto,rendezvousPublic crypto)
853 (asymmNonce dta)
854 (Right dta) -- using Asymm{senderKey} as remote key
855 let eOuter = fmap runIdentity $ uncomposed omsg0
856
857 anyRight [] f = return $ Left "parseDataToRoute: no user key"
858 anyRight (x:xs) f = f x >>= either (const $ anyRight xs f) (return . Right)
859
860 -- TODO: We don't currently have a way to look up which user key we
861 -- announced using along this onion route. Therefore, for now, we will
862 -- try all our user keys to see if any can decrypt the packet.
863 eInner <- case eOuter of
864 Left e -> return $ Left e
865 Right dtr -> anyRight ks $ \(sk,pk) -> do
866 omsg0 <- decryptMessage crypto
867 (sk,pk)
868 (asymmNonce dta)
869 (Left (dataFromKey dtr, dataToRoute dtr))
870 return $ do
871 omsg <- fmap runIdentity . uncomposed $ omsg0
872 Right (pk,dtr,omsg)
873
874 let e = do
875 (pk,dtr,omsg) <- eInner
876 return ( (pk, omsg)
877 , AnnouncedRendezvous
878 (dataFromKey dtr)
879 $ Rendezvous (rendezvousPublic crypto) $ onionNodeInfo od )
880 r = either (const $ Right (OnionToRouteResponse dta,od)) Left e
881 -- parseDataToRoute OnionToRouteResponse decipherAndAuth: auth fail
882 hPutStrLn stderr $ unlines
883 [ "parseDataToRoute " ++ either id (const "Right") e
884 , " crypto inner.me =" ++ either id (\(pk,_,_) -> show $ key2id pk) eInner
885 , " inner.you=" ++ either id (show . key2id . dataFromKey) eOuter
886 , " outer.me =" ++ show (key2id $ rendezvousPublic crypto)
887 , " outer.you=" ++ show (key2id $ senderKey dta)
888 ]
889 return r
890parseDataToRoute _ msg = return $ Right msg
891
892encodeDataToRoute :: TransportCrypto
893 -> ((PublicKey,OnionData),AnnouncedRendezvous)
894 -> IO (Maybe (OnionMessage Encrypted,OnionDestination r))
895encodeDataToRoute crypto ((me,omsg), AnnouncedRendezvous toxid (Rendezvous pub ni)) = do
896 nonce <- atomically $ transportNewNonce crypto
897 asel <- atomically $ selectAlias crypto (key2id me)
898 let (sk,pk) = case asel of
899 AnnouncingAlias sk pk -> (sk,pk)
900 _ -> (onionAliasSecret crypto, onionAliasPublic crypto)
901 innerSecret <- lookupSharedSecret crypto sk toxid nonce
902 let plain = encodePlain $ DataToRoute { dataFromKey = pk
903 , dataToRoute = ToxCrypto.encrypt innerSecret $ encodePlain omsg
904 }
905 outerSecret <- lookupSharedSecret crypto (onionAliasSecret crypto) pub nonce
906 let dta = ToxCrypto.encrypt outerSecret plain
907 hPutStrLn stderr $ unlines
908 [ "encodeDataToRoute me=" ++ show (key2id me)
909 , " dhtpk=" ++ case omsg of
910 OnionDHTPublicKey dmsg -> show (key2id $ dhtpk dmsg)
911 OnionFriendRequest fr -> "friend request"
912 , " ns=" ++ case omsg of
913 OnionDHTPublicKey dmsg -> show (dhtpkNodes dmsg)
914 OnionFriendRequest fr -> "friend request"
915 , " crypto inner.me =" ++ show (key2id pk)
916 , " inner.you=" ++ show (key2id toxid)
917 , " outer.me =" ++ show (key2id $ onionAliasPublic crypto)
918 , " outer.you=" ++ show (key2id pub)
919 , " " ++ show (AnnouncedRendezvous toxid (Rendezvous pub ni))
920 , " " ++ show dta
921 ]
922 return $ Just ( OnionToRoute toxid -- Public key of destination node
923 Asymm { senderKey = onionAliasPublic crypto
924 , asymmNonce = nonce
925 , asymmData = dta
926 }
927 , OnionDestination SearchingAlias ni Nothing )
diff --git a/src/Network/Tox/Transport.hs b/src/Network/Tox/Transport.hs
new file mode 100644
index 00000000..30df93c8
--- /dev/null
+++ b/src/Network/Tox/Transport.hs
@@ -0,0 +1,78 @@
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE GADTs #-}
3{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4{-# LANGUAGE KindSignatures #-}
5{-# LANGUAGE LambdaCase #-}
6{-# LANGUAGE ScopedTypeVariables #-}
7{-# LANGUAGE TupleSections #-}
8{-# LANGUAGE TypeOperators #-}
9module Network.Tox.Transport (toxTransport, RouteId) where
10
11import Network.QueryResponse
12import Crypto.Tox
13import Network.Tox.DHT.Transport
14import Network.Tox.Onion.Transport
15import Network.Tox.Crypto.Transport
16import OnionRouter
17
18import Network.Socket
19
20toxTransport ::
21 TransportCrypto
22 -> OnionRouter
23 -> (PublicKey -> IO (Maybe NodeInfo))
24 -> UDPTransport
25 -> IO ( Transport String NodeInfo (DHTMessage Encrypted8)
26 , Transport String (OnionDestination RouteId) (OnionMessage Encrypted)
27 , Transport String AnnouncedRendezvous (PublicKey,OnionData)
28 , Transport String SockAddr NetCrypto )
29toxTransport crypto orouter closeLookup udp = do
30 (dht,udp1) <- partitionTransportM (parseDHTAddr crypto) (fmap Just . encodeDHTAddr) $ forwardOnions crypto udp
31 (onion1,udp2) <- partitionTransportM (parseOnionAddr $ lookupSender orouter)
32 (encodeOnionAddr crypto $ lookupRoute orouter)
33 udp1
34 (dta,onion) <- partitionTransportM (parseDataToRoute crypto) (encodeDataToRoute crypto) onion1
35 let netcrypto = layerTransport parseNetCrypto encodeNetCrypto udp2
36 return ( forwardDHTRequests crypto closeLookup dht
37 , onion
38 , dta
39 , netcrypto
40 )
41
42
43-- instance (Sized a, Sized b) => Sized (a,b) where size = _todo
44
45
46-- Byte value Packet Kind Return address
47-- :----------- :--------------------
48-- `0x00` Ping Request DHTNode
49-- `0x01` Ping Response -
50-- `0x02` Nodes Request DHTNode
51-- `0x04` Nodes Response -
52-- `0x18` Cookie Request DHTNode, but without sending pubkey in response
53-- `0x19` Cookie Response - (no pubkey)
54--
55-- `0x21` LAN Discovery DHTNode (No reply, port 33445, trigger Nodes Request/Response)
56--
57-- `0x20` DHT Request DHTNode/-forward
58--
59-- `0x1a` Crypto Handshake CookieAddress
60--
61-- `0x1b` Crypto Data SessionAddress
62--
63-- `0x83` Announce Request OnionToOwner
64-- `0x84` Announce Response -
65-- `0x85` Onion Data Request OnionToOwner
66-- `0x86` Onion Data Response -
67--
68-- `0xf0` Bootstrap Info SockAddr?
69--
70-- `0x80` Onion Request 0 -forward
71-- `0x81` Onion Request 1 -forward
72-- `0x82` Onion Request 2 -forward
73-- `0x8c` Onion Response 3 -return
74-- `0x8d` Onion Response 2 -return
75-- `0x8e` Onion Response 1 -return
76
77
78
diff --git a/src/Network/UPNP.hs b/src/Network/UPNP.hs
new file mode 100644
index 00000000..ed6b4777
--- /dev/null
+++ b/src/Network/UPNP.hs
@@ -0,0 +1,39 @@
1module Network.UPNP where
2
3import Data.Maybe
4import Network.Address (sockAddrPort)
5import Network.Socket
6import System.Directory
7import System.IO
8import System.Process as Process
9
10protocols :: SocketType -> [String]
11protocols Stream = ["tcp"]
12protocols Datagram = ["udp"]
13protocols _ = ["udp","tcp"]
14
15upnpc :: FilePath
16upnpc = "/usr/bin/upnpc"
17
18-- | Invokes the miniupnpc command line program to request ports from a UPNP
19-- wifi router. Returns the process handle on success.
20requestPorts :: String -- ^ Description stored on router.
21 -> [(SocketType, SockAddr)] -- ^ Protocol-port pairs to request.
22 -> IO (Maybe ProcessHandle)
23requestPorts description binds = do
24 let requests = do
25 (stype,saddr) <- binds
26 proto <- protocols stype
27 port <- maybeToList (sockAddrPort saddr)
28 [ show port, proto ]
29 bail = return Nothing
30 case requests of
31 [] -> bail
32 _ -> do
33 gotMiniUPNPC <- doesFileExist upnpc
34 if gotMiniUPNPC then do
35 phandle <- spawnProcess upnpc $ "-e": description : "-r" : requests
36 return $ Just phandle
37 else do
38 hPutStrLn stderr $ "Warning: unable to find miniupnpc client at "++upnpc++"."
39 bail
diff --git a/src/StaticAssert.hs b/src/StaticAssert.hs
new file mode 100644
index 00000000..d0784c97
--- /dev/null
+++ b/src/StaticAssert.hs
@@ -0,0 +1,13 @@
1module StaticAssert where
2
3import Network.Socket (htonl)
4import Language.Haskell.TH
5
6staticAssert :: Bool -> Q [Dec]
7staticAssert cond = case cond of
8 True -> return []
9 False -> fail "staticAssert failed"
10
11isLittleEndian :: Bool
12isLittleEndian = htonl 0x01000000 == 1
13
diff --git a/src/System/Global6.hs b/src/System/Global6.hs
new file mode 100644
index 00000000..f70a8547
--- /dev/null
+++ b/src/System/Global6.hs
@@ -0,0 +1,28 @@
1module System.Global6 where
2
3import Control.Monad
4import Data.IP
5import Data.List
6import Data.Maybe
7import Network.Socket
8import System.Process
9import Text.Read
10
11parseIpAddr :: String -> Maybe IPv6
12parseIpAddr s = do
13 let ws = words s
14 (addr,bs) = splitAt 1 $ drop 1 $ dropWhile (/= "inet6") ws
15 guard ("global" `elem` bs)
16 addr <- listToMaybe addr
17 guard (not $ isPrefixOf "fd" addr)
18 guard (not $ isPrefixOf "fc" addr)
19 let (addr',slash) = break (=='/') addr
20 ip6 <- readMaybe addr'
21 return $ (ip6 :: IPv6)
22
23
24global6 :: IO (Maybe IPv6)
25global6 = do
26 addrs <- lines <$> readProcess "ip" ["-o","-6","addr"] ""
27 return $ foldr1 mplus $ map parseIpAddr addrs
28
diff --git a/src/Text/XXD.hs b/src/Text/XXD.hs
new file mode 100644
index 00000000..d835b238
--- /dev/null
+++ b/src/Text/XXD.hs
@@ -0,0 +1,26 @@
1module Text.XXD where
2
3import qualified Data.ByteString.Base16 as Base16
4import Data.ByteString (ByteString)
5import qualified Data.ByteString as B
6import Data.Word
7import Data.Bits
8import Data.Char
9import Text.Printf
10
11nibble :: Word8 -> Char
12nibble b = intToDigit (fromIntegral (b .&. 0x0F))
13
14xxd :: Int -> ByteString -> [String]
15xxd offset bs | B.null bs = []
16xxd offset bs = printf "%03x: %s" offset ds : xxd (offset + B.length xs) bs'
17 where
18 ds = unwords $ map (\byte -> [nibble (byte `shiftR` 4), nibble byte])
19 $ B.unpack xs
20 (xs,bs') = B.splitAt 16 bs
21
22{-
23main = do
24 bs <- B.getContents
25 mapM_ putStrLn $ xxd 0 bs
26 -}
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 00000000..75c046e0
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,54 @@
1tox: Add fallback trials to cookie response in case response is from another address than request.
2
3ui: Online help.
4
5ui: Explicit routing table node deletion. "forget" command.
6
7ui: a - with no arguments would list the currently active recuring publications.
8
9kademlia: Change refresh algorithm to refresh farther away buckets before closer ones.
10
11kademlia: Remove (without replacement) stale routing nodes at some point.
12
13bug: Why does running without -4 (ipv4-only) on an ipv6-disabled computer prevent
14 storage of bt peers and tox keys in the local store?
15
16kademlia: Give different networks a different minimum count to terminate
17 bootstrap. Imperically, tox4: 6 buckets, tox6: 3 buckets
18
19tox: Don't store ourself in the kademlia buckets.
20
21tox: fallback to https://nodes.tox.chat/json
22
23tox: bootstrap motd query
24
25tox: hardening get-nodes test request.
26
27tox: nat ping
28
29tox: cache diffie-helman secrets
30
31tox: Expire ofline Tox announces.
32
33tox: Chat support.
34
35bt: Collect PeerStore garbage: "Note that you should call .put() every hour for
36 content that you want to keep alive, since nodes may discard data nodes
37 older than 2 hours." (source: https://www.npmjs.com/package/bittorrent-dht)
38
39bt: Limit peers in get_peers response for UDP packet size limiting (around 1k).
40
41bt: Use LMDB backend for peer store (and nodes too?).
42
43maint: Rename files.
44
45 OnionRouter -> Network.Tox.Onion.Routes
46 Announcer -> Network.Kademlia.Announce
47 InterruptibleDelay -> Control.Concurrent.Delay
48 Roster -> Network.Tox.ContactInfo
49 Crypto.Tox -> Network.Tox.Crypto
50 Network.Tox.Crypto.Handlers -> Network.Tox.Friend.Handlers
51 Network.Tox.Crypto.Transport -> Network.Tox.Friend.Transport
52
53maint: Probably, Network.* should be reserved for very general tools and the Tox and
54 BitTorrent paths should be moved to the top level.
diff --git a/vnet/build.sh b/vnet/build.sh
new file mode 100755
index 00000000..36818611
--- /dev/null
+++ b/vnet/build.sh
@@ -0,0 +1,89 @@
1#!/bin/sh
2
3wire() {
4 ip link add $1 \
5 type veth \
6 peer name $2
7}
8
9set -x
10
11# Not using this...
12buildhub() {
13 wire hub0 ep0
14 wire hub1 ep1
15 wire hub2 ep2
16 wire hub3 ep3
17 wire hub4 ep4
18 wire hub5 ep5
19 wire hub6 ep6
20 wire hub7 ep7
21
22 wire hub8 ep8
23 wire hub9 ep9
24 wire hubA epA
25 wire hubB epB
26 wire hubC epC
27 wire hubD epD
28 wire hubE epE
29 wire hubF epF
30
31 brctl addbr hub
32 brctl addif hub hub0
33 brctl addif hub hub1
34 brctl addif hub hub2
35 brctl addif hub hub3
36 brctl addif hub hub4
37 brctl addif hub hub5
38 brctl addif hub hub6
39 brctl addif hub hub7
40 brctl addif hub hub8
41 brctl addif hub hub9
42 brctl addif hub hubA
43 brctl addif hub hubB
44 brctl addif hub hubC
45 brctl addif hub hubD
46 brctl addif hub hubE
47 brctl addif hub hubF
48
49 ip addr add 222.0.0.1 dev hub
50}
51
52# This is simpler...
53buildtaps() {
54 ip tuntap add ep0 mode tap
55 ip tuntap add ep1 mode tap
56 ip tuntap add ep2 mode tap
57 ip tuntap add ep3 mode tap
58 ip tuntap add ep4 mode tap
59 ip tuntap add ep5 mode tap
60 ip tuntap add ep6 mode tap
61 ip tuntap add ep7 mode tap
62 ip tuntap add ep8 mode tap
63 ip tuntap add ep9 mode tap
64 ip tuntap add epA mode tap
65 ip tuntap add epB mode tap
66 ip tuntap add epC mode tap
67 ip tuntap add epD mode tap
68 ip tuntap add epE mode tap
69 ip tuntap add epF mode tap
70}
71
72buildtaps
73
74ip addr add 80.99.99.99 dev ep0
75ip addr add 81.99.99.99 dev ep1
76ip addr add 82.99.99.99 dev ep2
77ip addr add 83.99.99.99 dev ep3
78ip addr add 84.99.99.99 dev ep4
79ip addr add 85.99.99.99 dev ep5
80ip addr add 86.99.99.99 dev ep6
81ip addr add 87.99.99.99 dev ep7
82ip addr add 88.99.99.99 dev ep8
83ip addr add 89.99.99.99 dev ep9
84ip addr add 90.99.99.99 dev epA
85ip addr add 91.99.99.99 dev epB
86ip addr add 92.99.99.99 dev epC
87ip addr add 93.99.99.99 dev epD
88ip addr add 94.99.99.99 dev epE
89ip addr add 95.99.99.99 dev epF
diff --git a/vnet/clean.sh b/vnet/clean.sh
new file mode 100755
index 00000000..3e0af1bd
--- /dev/null
+++ b/vnet/clean.sh
@@ -0,0 +1,49 @@
1#!/bin/sh
2
3cleanhub() {
4 brctl delbr hub
5
6 ip link del hub0
7 ip link del hub1
8 ip link del hub2
9 ip link del hub3
10 ip link del hub4
11 ip link del hub5
12 ip link del hub6
13 ip link del hub7
14
15 ip link del hub8
16 ip link del hub9
17 ip link del hubA
18 ip link del hubB
19 ip link del hubC
20 ip link del hubD
21 ip link del hubE
22 ip link del hubF
23}
24
25cleantaps()
26{
27 ip link del ep0
28 ip link del ep1
29 ip link del ep2
30 ip link del ep3
31 ip link del ep4
32 ip link del ep5
33 ip link del ep6
34 ip link del ep7
35
36 ip link del ep8
37 ip link del ep9
38 ip link del epA
39 ip link del epB
40 ip link del epC
41 ip link del epD
42 ip link del epE
43 ip link del epF
44}
45
46cleantaps
47
48rm -rf ep0 ep1 ep2 ep3 ep4 ep5 ep6 ep7 \
49 ep8 ep9 epA epB epC epD epE epF
diff --git a/vnet/everywhere.sh b/vnet/everywhere.sh
new file mode 100755
index 00000000..ebd4ca38
--- /dev/null
+++ b/vnet/everywhere.sh
@@ -0,0 +1,8 @@
1#!/bin/sh
2cmd="$@"
3for ep in ep?
4do
5 ( cd $ep
6 $cmd
7 )
8done
diff --git a/vnet/mkroutes.sh b/vnet/mkroutes.sh
new file mode 100755
index 00000000..4058bb40
--- /dev/null
+++ b/vnet/mkroutes.sh
@@ -0,0 +1,19 @@
1#!/bin/sh
2ip=$1
3ip route add 80.99.99.99/32 via $host
4ip route add 80.99.99.99/32 via $host
5ip route add 81.99.99.99/32 via $host
6ip route add 82.99.99.99/32 via $host
7ip route add 83.99.99.99/32 via $host
8ip route add 84.99.99.99/32 via $host
9ip route add 85.99.99.99/32 via $host
10ip route add 86.99.99.99/32 via $host
11ip route add 87.99.99.99/32 via $host
12ip route add 88.99.99.99/32 via $host
13ip route add 89.99.99.99/32 via $host
14ip route add 90.99.99.99/32 via $host
15ip route add 91.99.99.99/32 via $host
16ip route add 92.99.99.99/32 via $host
17ip route add 93.99.99.99/32 via $host
18ip route add 94.99.99.99/32 via $host
19ip route add 95.99.99.99/32 via $host
diff --git a/vnet/run.sh b/vnet/run.sh
new file mode 100755
index 00000000..0189b6b7
--- /dev/null
+++ b/vnet/run.sh
@@ -0,0 +1,45 @@
1#!/bin/sh
2
3[ -L ./dhtd ] || ( echo "Missing symbolic link to your build: ./dhtd" ; exit 1 )
4
5mkdir -p ep0 ep1 ep2 ep3 ep4 ep5 ep6 ep7 \
6 ep8 ep9 epA epB epC epD epE epF
7
8rm -f window-count
9echo detach > detached
10
11screen -S test -s ./screen-shell.sh -c detached
12sleep 0.5
13screen -S test -X screen
14sleep 0.5
15screen -S test -X screen
16sleep 0.5
17screen -S test -X screen
18sleep 0.5
19
20screen -S test -X screen
21sleep 0.5
22screen -S test -X screen
23sleep 0.5
24screen -S test -X screen
25sleep 0.5
26screen -S test -X screen
27sleep 0.5
28
29screen -S test -X screen
30sleep 0.5
31screen -S test -X screen
32sleep 0.5
33screen -S test -X screen
34sleep 0.5
35screen -S test -X screen
36sleep 0.5
37
38screen -S test -X screen
39sleep 0.5
40screen -S test -X screen
41sleep 0.5
42screen -S test -X screen
43sleep 0.5
44screen -S test -X screen
45sleep 0.5
diff --git a/vnet/screen-everywhere.sh b/vnet/screen-everywhere.sh
new file mode 100755
index 00000000..e4f75374
--- /dev/null
+++ b/vnet/screen-everywhere.sh
@@ -0,0 +1,21 @@
1#!/bin/sh
2cmd="$@"
3echo "$cmd" > /tmp/screen-exchange
4screen -S test -X eval readbuf \
5 "select 15" "paste ." \
6 "select 14" "paste ." \
7 "select 13" "paste ." \
8 "select 12" "paste ." \
9 "select 11" "paste ." \
10 "select 10" "paste ." \
11 "select 9" "paste ." \
12 "select 8" "paste ." \
13 "select 7" "paste ." \
14 "select 6" "paste ." \
15 "select 5" "paste ." \
16 "select 5" "paste ." \
17 "select 4" "paste ." \
18 "select 3" "paste ." \
19 "select 2" "paste ." \
20 "select 1" "paste ." \
21 "select 0" "paste ."
diff --git a/vnet/screen-shell.sh b/vnet/screen-shell.sh
new file mode 100755
index 00000000..6143c81d
--- /dev/null
+++ b/vnet/screen-shell.sh
@@ -0,0 +1,14 @@
1#!/bin/sh
2ifaddr() {
3 ip -o address show dev $1 | sed -n 's#.* inet \([^ /]*\).*#\1#p'
4}
5touch window-count
6count=$(cat window-count)
7count=${count:-(-1)}
8count=$(( $count + 1 ))
9echo $count > window-count
10digit=$(printf '%X' $count)
11cd ep$digit
12port=$(( 33400 + $count ))
13pwd
14../dhtd tox=$(ifaddr ep$digit):$port,bt=
diff --git a/vnet/show-links.sh b/vnet/show-links.sh
new file mode 100755
index 00000000..d7115a0b
--- /dev/null
+++ b/vnet/show-links.sh
@@ -0,0 +1,2 @@
1#!/bin/sh
2ip -o link | sed 's/\([^:]*:[^:]*:*\).*/\1/'