diff options
162 files changed, 29695 insertions, 0 deletions
@@ -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 | |||
@@ -1 +1,18 @@ | |||
1 | nohup.out | ||
2 | |||
3 | dist | ||
4 | cabal-dev | ||
5 | .cabal-sandbox | ||
6 | cabal.sandbox.config | ||
7 | tmp | ||
8 | data | ||
9 | upload-docs | ||
10 | *.torrent | ||
11 | *.aux | ||
12 | *.eventlog | ||
13 | *.hp | ||
14 | |||
15 | *.ps | ||
16 | *.prof | ||
17 | res/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 @@ | |||
1 | Sam Truzjan <pxqr.sta@gmail.com> | ||
2 | Sam 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 @@ | |||
1 | language: haskell | ||
2 | |||
3 | ghc: | ||
4 | - 7.6 | ||
5 | |||
6 | before_install: | ||
7 | - sudo apt-get install rtorrent screen | ||
8 | - rtorrent -h | grep version | ||
9 | - screen --version || true | ||
10 | |||
11 | install: | ||
12 | - cabal sandbox init | ||
13 | - ./dev/update-dependencies.sh | ||
14 | |||
15 | script: | ||
16 | - cabal configure && cabal build | ||
17 | - cabal configure -ftesting --enable-tests --enable-benchmark && cabal build && ./dist/build/spec/spec | ||
18 | |||
19 | notifications: | ||
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 #-} | ||
6 | module Announcer | ||
7 | ( Announcer | ||
8 | , AnnounceKey | ||
9 | , packAnnounceKey | ||
10 | , unpackAnnounceKey | ||
11 | , AnnounceMethod(..) | ||
12 | , forkAnnouncer | ||
13 | , stopAnnouncer | ||
14 | , schedule | ||
15 | , cancel | ||
16 | ) where | ||
17 | |||
18 | import qualified Data.MinMaxPSQ as MM | ||
19 | import Data.Wrapper.PSQ as PSQ | ||
20 | import InterruptibleDelay | ||
21 | import Network.Kademlia.Routing as R | ||
22 | import Network.Kademlia.Search | ||
23 | |||
24 | import Control.Concurrent.Lifted.Instrument | ||
25 | import Control.Concurrent.STM | ||
26 | import Control.Monad | ||
27 | import Data.ByteString (ByteString) | ||
28 | import qualified Data.ByteString.Char8 as Char8 | ||
29 | import Data.Function | ||
30 | import Data.Hashable | ||
31 | import Data.Maybe | ||
32 | import Data.Ord | ||
33 | import Data.Time.Clock.POSIX | ||
34 | |||
35 | newtype AnnounceKey = AnnounceKey ByteString | ||
36 | deriving (Hashable,Ord,Eq) | ||
37 | |||
38 | packAnnounceKey :: Announcer -> String -> STM AnnounceKey | ||
39 | packAnnounceKey _ = return . AnnounceKey . Char8.pack | ||
40 | |||
41 | unpackAnnounceKey :: AnnounceKey -> AnnounceKey -> STM String | ||
42 | unpackAnnounceKey _ (AnnounceKey bs) = return $ Char8.unpack bs | ||
43 | |||
44 | data ScheduledItem | ||
45 | = StopAnnouncer | ||
46 | | NewAnnouncement (STM (IO ())) (IO ()) (IO ()) POSIXTime | ||
47 | | SearchFinished (IO ()) (IO ()) POSIXTime | ||
48 | | Announce (STM (IO ())) (IO ()) POSIXTime | ||
49 | | DeleteAnnouncement | ||
50 | |||
51 | data Announcer = Announcer | ||
52 | { scheduled :: TVar (PSQ' AnnounceKey POSIXTime ScheduledItem) | ||
53 | , announcerActive :: TVar Bool | ||
54 | , interrutible :: InterruptibleDelay | ||
55 | } | ||
56 | |||
57 | announceK :: Int | ||
58 | announceK = 8 | ||
59 | |||
60 | data 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 | |||
65 | scheduleImmediately :: Announcer -> AnnounceKey -> ScheduledItem -> STM () | ||
66 | scheduleImmediately announcer k item | ||
67 | = modifyTVar' (scheduled announcer) (PSQ.insert' k item 0) | ||
68 | |||
69 | stopAnnouncer :: Announcer -> IO () | ||
70 | stopAnnouncer announcer = do | ||
71 | atomically $ scheduleImmediately announcer (AnnounceKey "*stop*") StopAnnouncer | ||
72 | interruptDelay (interrutible announcer) | ||
73 | atomically $ readTVar (announcerActive announcer) >>= check . not | ||
74 | |||
75 | data 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 | |||
90 | schedule :: Announcer -> AnnounceKey -> AnnounceMethod r -> r -> IO () | ||
91 | schedule 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 | |||
128 | cancel :: Announcer -> AnnounceKey -> AnnounceMethod r -> r -> IO () | ||
129 | cancel announcer k _ _ = do | ||
130 | atomically $ scheduleImmediately announcer k $ DeleteAnnouncement | ||
131 | interruptDelay (interrutible announcer) | ||
132 | |||
133 | forkAnnouncer :: IO Announcer | ||
134 | forkAnnouncer = 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 | |||
143 | announceThread :: Announcer -> IO () | ||
144 | announceThread 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 | |||
167 | performScheduledItem :: Announcer -> POSIXTime -> Binding' AnnounceKey POSIXTime ScheduledItem -> STM (Maybe (IO ())) | ||
168 | performScheduledItem 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 @@ | |||
1 | 2014-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 | |||
11 | 2014-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 | |||
22 | 2013-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 | |||
32 | 2013-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 | |||
38 | 2013-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 | |||
47 | 2013-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 | |||
60 | 2013-11-01 Sam Truzjan <pxqr.sta@gmail.com> | ||
61 | |||
62 | bittorrent | ||
63 | |||
64 | Initial version: 0.0.0.1 | ||
65 | |||
66 | 2013-10-17 Sam Truzjan <pxqr.sta@gmail.com> | ||
67 | |||
68 | krpc | ||
69 | |||
70 | * 0.4.1.0: Use bencoding-0.4.* | ||
71 | |||
72 | 2013-10-03 Sam Truzjan <pxqr.sta@gmail.com> | ||
73 | |||
74 | krpc | ||
75 | |||
76 | * 0.4.0.1: Minor documentation fixes. | ||
77 | |||
78 | 2013-10-03 Sam Truzjan <pxqr.sta@gmail.com> | ||
79 | |||
80 | krpc | ||
81 | |||
82 | * 0.4.0.0: IPv6 support. | ||
83 | |||
84 | 2013-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 | |||
91 | 2013-09-28 Sam Truzjan <pxqr.sta@gmail.com> | ||
92 | |||
93 | krpc | ||
94 | |||
95 | * 0.2.2.0: Use bencoding-0.2.2.* | ||
96 | |||
97 | 2013-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 | |||
105 | 2013-07-09 Sam Truzjan <pxqr.sta@gmail.com> | ||
106 | |||
107 | krpc | ||
108 | |||
109 | * 0.1.1.0: Allow passing raw argument\/result dictionaries. | ||
110 | |||
111 | 2013-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 @@ | |||
1 | module InterruptibleDelay where | ||
2 | |||
3 | import Control.Concurrent | ||
4 | import Control.Monad | ||
5 | import Control.Exception ({-evaluate,-}handle,ErrorCall(..)) | ||
6 | import Data.Time.Clock (NominalDiffTime) | ||
7 | |||
8 | type Microseconds = Int | ||
9 | |||
10 | microseconds :: NominalDiffTime -> Microseconds | ||
11 | microseconds d = round $ 1000000 * d | ||
12 | |||
13 | data InterruptibleDelay = InterruptibleDelay | ||
14 | { delayThread :: MVar ThreadId | ||
15 | } | ||
16 | |||
17 | interruptibleDelay :: IO InterruptibleDelay | ||
18 | interruptibleDelay = do | ||
19 | fmap InterruptibleDelay newEmptyMVar | ||
20 | |||
21 | startDelay :: InterruptibleDelay -> Microseconds -> IO Bool | ||
22 | startDelay 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 | |||
35 | interruptDelay :: InterruptibleDelay -> IO () | ||
36 | interruptDelay 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 @@ | |||
1 | Copyright (c) 2013, Sam Truzjan | ||
2 | |||
3 | All rights reserved. | ||
4 | |||
5 | Redistribution and use in source and binary forms, with or without | ||
6 | modification, 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 | |||
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
30 | OF 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 | |||
3 | clean: | ||
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 #-} | ||
3 | module OnionRouter where | ||
4 | |||
5 | import Control.Concurrent.Lifted.Instrument | ||
6 | import Crypto.Tox | ||
7 | import Network.Address | ||
8 | import Network.Kademlia | ||
9 | import Network.Kademlia.Routing | ||
10 | import Network.QueryResponse | ||
11 | import Network.Tox.NodeId | ||
12 | import Network.Tox.Onion.Transport | ||
13 | |||
14 | import Control.Arrow | ||
15 | import Control.Concurrent.STM | ||
16 | import Control.Monad | ||
17 | import Crypto.PubKey.Curve25519 | ||
18 | import Crypto.Random | ||
19 | import Data.Bits | ||
20 | import qualified Data.ByteString as B | ||
21 | import Data.Hashable | ||
22 | import qualified Data.HashMap.Strict as HashMap | ||
23 | ;import Data.HashMap.Strict (HashMap) | ||
24 | import qualified Data.IntMap as IntMap | ||
25 | ;import Data.IntMap (IntMap) | ||
26 | import Data.Maybe | ||
27 | import qualified Data.Serialize as S | ||
28 | import Data.Typeable | ||
29 | import Data.Word | ||
30 | import qualified Data.Word64Map as W64 | ||
31 | ;import Data.Word64Map (Word64Map, fitsInInt) | ||
32 | import Network.Socket | ||
33 | import System.Endian | ||
34 | import 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. | ||
47 | data 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 | |||
84 | data 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. | ||
100 | timeoutForRoute :: RouteRecord -> Int | ||
101 | timeoutForRoute RouteRecord{ responseCount = 0 } = 4000000 | ||
102 | timeoutForRoute RouteRecord{ responseCount = _ } = 10000000 | ||
103 | |||
104 | freshRoute :: OnionRoute -> RouteRecord | ||
105 | freshRoute r = RouteRecord | ||
106 | { storedRoute = r | ||
107 | , responseCount = 0 | ||
108 | , timeoutCount = 0 | ||
109 | } | ||
110 | |||
111 | gotResponse :: RouteRecord -> RouteRecord | ||
112 | gotResponse rr = rr | ||
113 | { responseCount = succ $ responseCount rr | ||
114 | , timeoutCount = 0 | ||
115 | } | ||
116 | |||
117 | gotTimeout :: RouteRecord -> RouteRecord | ||
118 | gotTimeout rr = rr | ||
119 | { timeoutCount = succ $ timeoutCount rr | ||
120 | } | ||
121 | |||
122 | data RouteEvent = BuildRoute RouteId | ||
123 | |||
124 | newOnionRouter :: (String -> IO ()) -> IO OnionRouter | ||
125 | newOnionRouter 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 | |||
152 | forkRouteBuilder :: OnionRouter -> (NodeId -> NodeInfo -> IO (Maybe [NodeInfo])) -> IO OnionRouter | ||
153 | forkRouteBuilder 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 | |||
171 | generateNodeId :: MonadRandom m => m NodeId | ||
172 | generateNodeId = either (error "unable to make random nodeid") | ||
173 | id | ||
174 | . S.decode <$> getRandomBytes 32 | ||
175 | |||
176 | distinct3by :: Eq t => (a -> t) -> a -> a -> a -> Bool | ||
177 | distinct3by 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. | ||
180 | randomR :: (DRG g, Integral a) => (a, a) -> g -> (a, g) | ||
181 | randomR (l,h) = randomIvalInteger (toInteger l, toInteger h) | ||
182 | |||
183 | next :: DRG g => g -> (Int,g) | ||
184 | next 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 | |||
189 | randomIvalInteger :: (DRG g, Num a) => (Integer, Integer) -> g -> (a, g) | ||
190 | randomIvalInteger (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 | |||
213 | selectTrampolines :: OnionRouter -> IO [NodeInfo] | ||
214 | selectTrampolines 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 | |||
229 | selectTrampolines' :: OnionRouter -> STM (Either [NodeInfo] [NodeInfo]) | ||
230 | selectTrampolines' 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 | |||
251 | handleEvent :: (NodeId -> NodeInfo -> IO (Maybe [NodeInfo])) -> OnionRouter -> RouteEvent -> IO () | ||
252 | handleEvent 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 | |||
327 | lookupSender :: OnionRouter -> SockAddr -> Nonce8 -> IO (Maybe (OnionDestination RouteId)) | ||
328 | lookupSender 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 | |||
342 | lookupRoute :: OnionRouter -> NodeInfo -> RouteId -> IO (Maybe OnionRoute) | ||
343 | lookupRoute or ni (RouteId rid) = do | ||
344 | mb <- atomically $ IntMap.lookup rid <$> readTVar (routeMap or) | ||
345 | return $ storedRoute <$> mb | ||
346 | |||
347 | lookupTimeout :: OnionRouter -> Nonce8 -> OnionDestination r -> STM (OnionDestination RouteId, Int) | ||
348 | lookupTimeout 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 | |||
356 | hookQueries :: OnionRouter -> (tid -> Nonce8) | ||
357 | -> TransactionMethods d tid (OnionDestination r) x | ||
358 | -> TransactionMethods d tid (OnionDestination r) x | ||
359 | hookQueries 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 | |||
397 | hookBucketList :: KademliaSpace NodeId NodeInfo -> TVar (BucketList NodeInfo) -> OnionRouter -> RoutingTransition NodeInfo -> STM () | ||
398 | hookBucketList 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) | ||
422 | hookBucketList _ _ 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 () | ||
434 | hookBucketList _ _ _ _ = return () -- ignore Applicant event. | ||
435 | |||
436 | newtype IPClass = IPClass Word32 | ||
437 | deriving Eq | ||
438 | |||
439 | ipkey :: IPClass -> Int | ||
440 | ipkey (IPClass k) = fromIntegral k | ||
441 | |||
442 | nodeClass :: NodeInfo -> IPClass | ||
443 | nodeClass = ipClass. nodeAddr | ||
444 | |||
445 | ipClass :: SockAddr -> IPClass | ||
446 | ipClass= either ipClass' ipClass' . either4or6 | ||
447 | |||
448 | ipClass' :: SockAddr -> IPClass | ||
449 | ipClass' (SockAddrInet _ addr) = IPClass (fromBE32 addr .&. 0xFF000000) | ||
450 | ipClass' (SockAddrInet6 _ _ (hiword,_,_,_) _) = IPClass hiword | ||
451 | ipClass' _ = 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 #-} | ||
2 | module Roster where | ||
3 | |||
4 | import Control.Concurrent.STM | ||
5 | import Control.Monad | ||
6 | import Crypto.PubKey.Curve25519 | ||
7 | import qualified Data.HashMap.Strict as HashMap | ||
8 | ;import Data.HashMap.Strict (HashMap) | ||
9 | import Data.Maybe | ||
10 | import Network.Tox.DHT.Transport as DHT | ||
11 | import Network.Tox.NodeId | ||
12 | import Network.Tox.Onion.Transport as Onion | ||
13 | import System.IO | ||
14 | |||
15 | newtype Roster = Roster { accounts :: TVar (HashMap NodeId Account) } | ||
16 | |||
17 | data Account = Account | ||
18 | { userSecret :: SecretKey -- local secret key | ||
19 | , contacts :: TVar (HashMap NodeId Contact) -- received contact info | ||
20 | } | ||
21 | |||
22 | data Contact = Contact | ||
23 | { contactKeyPacket :: Maybe (DHT.DHTPublicKey) | ||
24 | , contactFriendRequest :: Maybe (DHT.FriendRequest) | ||
25 | } | ||
26 | |||
27 | mergeContact :: Contact -> Maybe Contact -> Maybe Contact | ||
28 | mergeContact (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 | ||
37 | mergeContact new Nothing = Just new | ||
38 | |||
39 | newRoster :: IO Roster | ||
40 | newRoster = atomically $ Roster <$> newTVar HashMap.empty | ||
41 | |||
42 | newAccount :: SecretKey -> STM Account | ||
43 | newAccount sk = Account sk <$> newTVar HashMap.empty | ||
44 | |||
45 | addRoster :: Roster -> SecretKey -> STM () | ||
46 | addRoster (Roster as) sk = do | ||
47 | a <- newAccount sk | ||
48 | modifyTVar' as $ HashMap.insert (key2id $ toPublic sk) a | ||
49 | |||
50 | delRoster :: Roster -> PublicKey -> STM () | ||
51 | delRoster (Roster as) pk = modifyTVar' as $ HashMap.delete (key2id pk) | ||
52 | |||
53 | updateRoster :: Roster -> Onion.AnnouncedRendezvous -> (PublicKey,Onion.OnionData) -> IO () | ||
54 | updateRoster 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 | |||
63 | updateAccount :: PublicKey -> Onion.OnionData -> Account -> STM () | ||
64 | updateAccount remoteUserKey (Onion.OnionDHTPublicKey dhtpk) acc = do | ||
65 | modifyTVar' (contacts acc) $ HashMap.alter (mergeContact $ Contact (Just dhtpk) Nothing) | ||
66 | (key2id remoteUserKey) | ||
67 | |||
68 | updateAccount remoteUserKey (Onion.OnionFriendRequest fr) acc = do | ||
69 | modifyTVar' (contacts acc) $ HashMap.alter (mergeContact $ Contact Nothing (Just fr)) | ||
70 | (key2id remoteUserKey) | ||
71 | |||
72 | dnsPresentation :: Roster -> STM String | ||
73 | dnsPresentation (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 | |||
84 | dnsPresentation1 :: (NodeId,DHTPublicKey) -> String | ||
85 | dnsPresentation1 (nid,dk) = unlines | ||
86 | [ concat [ show nid, ".tox. IN CNAME ", show (key2id $ dhtpk dk), ".dht." ] | ||
87 | ] | ||
88 | |||
89 | type LocalKey = NodeId | ||
90 | type RemoteKey = NodeId | ||
91 | |||
92 | friendRequests :: Roster -> STM (HashMap LocalKey [(RemoteKey,DHT.FriendRequest)]) | ||
93 | friendRequests (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 | ||
7 | data 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 | |||
3 | A [BitTorrent][0] library implementation. It allows to read/write | ||
4 | torrent files, transfer data files, query trackers and DHT. The | ||
5 | library is still in active development and have some subsystems | ||
6 | partially implemented. | ||
7 | |||
8 | For 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 | |||
17 | The protocol has [many enchancements][bep-list]. This table keep track | ||
18 | if 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 | |||
61 | The 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 | |||
69 | Some 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 @@ | |||
1 | Layout | ||
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 #-} | ||
4 | module Main (main) where | ||
5 | |||
6 | import Control.DeepSeq | ||
7 | import Network | ||
8 | import Control.Monad | ||
9 | import Control.Monad.Logger | ||
10 | import Control.Monad.Reader | ||
11 | import Criterion.Main | ||
12 | import Data.ByteString as BS | ||
13 | import Network.DatagramServer | ||
14 | |||
15 | |||
16 | import Network.BitTorrent.Exchange.Protocol as BT | ||
17 | import Data.Torrent.Block as BT | ||
18 | import Data.Torrent.Bitfield as BT | ||
19 | |||
20 | instance KRPC ByteString ByteString where | ||
21 | method = "echo" | ||
22 | |||
23 | instance MonadLogger IO where | ||
24 | monadLoggerLog _ _ _ _ = return () | ||
25 | |||
26 | |||
27 | instance NFData PortNumber where | ||
28 | rnf = rnf . (fromIntegral :: PortNumber -> Int) | ||
29 | |||
30 | instance NFData BlockIx where | ||
31 | rnf (BlockIx a b c) = a `deepseq` b `deepseq` rnf c | ||
32 | |||
33 | instance NFData Block where | ||
34 | rnf (Block a b c) = a `deepseq` b `deepseq` rnf c | ||
35 | |||
36 | instance NFData Bitfield | ||
37 | |||
38 | instance 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 | {- | ||
48 | encodeMessages :: [Message] -> ByteString | ||
49 | encodeMessages xs = runPut (mapM_ put xs) | ||
50 | |||
51 | decodeMessages :: ByteString -> Either String [Message] | ||
52 | decodeMessages = runGet (many get) | ||
53 | -} | ||
54 | |||
55 | echo :: Handler IO | ||
56 | echo = handler $ \ _ bs -> return (bs :: ByteString) | ||
57 | |||
58 | addr :: SockAddr | ||
59 | addr = SockAddrInet 6000 (256 * 256 * 256 + 127) | ||
60 | |||
61 | -- main :: IO () | ||
62 | -- main = defaultMain [] | ||
63 | main :: IO () | ||
64 | main = 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 #-} | ||
3 | module Main (main) where | ||
4 | |||
5 | import Control.Concurrent | ||
6 | import Data.Bitfield | ||
7 | import Network.BitTorrent | ||
8 | import System.Environment | ||
9 | import Control.Monad.Reader | ||
10 | import Data.IORef | ||
11 | |||
12 | |||
13 | main :: IO () | ||
14 | main = 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 #-} | ||
2 | module Main (main) where | ||
3 | |||
4 | import Data.BEncode | ||
5 | import Data.ByteString as BS | ||
6 | import Data.Torrent | ||
7 | import Criterion.Main | ||
8 | |||
9 | |||
10 | tinyPath :: FilePath | ||
11 | tinyPath = "res/dapper-dvd-amd64.iso.torrent" | ||
12 | |||
13 | largePath :: FilePath | ||
14 | largePath = "res/pkg.torrent" | ||
15 | |||
16 | decoder :: ByteString -> Torrent | ||
17 | decoder bs = let Right r = decode bs in r | ||
18 | |||
19 | main :: IO () | ||
20 | main = 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 @@ | |||
1 | name: bittorrent | ||
2 | version: 0.0.0.3 | ||
3 | license: BSD3 | ||
4 | license-file: LICENSE | ||
5 | author: Sam Truzjan | ||
6 | maintainer: Sam Truzjan <pxqr.sta@gmail.com> | ||
7 | copyright: (c) 2013, Sam Truzjan | ||
8 | category: Network | ||
9 | build-type: Custom | ||
10 | cabal-version: >= 1.10 | ||
11 | tested-with: GHC == 7.6.3 | ||
12 | homepage: https://github.com/cobit/bittorrent | ||
13 | bug-reports: https://github.com/cobit/bittorrent/issues | ||
14 | synopsis: BitTorrent protocol implementation. | ||
15 | description: | ||
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 | |||
22 | extra-source-files: res/dapper-dvd-amd64.iso.torrent | ||
23 | res/pkg.torrent | ||
24 | README.md | ||
25 | ChangeLog | ||
26 | cbits/*.h | ||
27 | |||
28 | |||
29 | source-repository head | ||
30 | type: git | ||
31 | location: git://github.com/cobit/bittorrent.git | ||
32 | |||
33 | source-repository this | ||
34 | type: git | ||
35 | location: git://github.com/cobit/bittorrent.git | ||
36 | branch: master | ||
37 | tag: v0.0.0.3 | ||
38 | |||
39 | flag testing | ||
40 | description: Whether to build tests. | ||
41 | default: False | ||
42 | |||
43 | flag examples | ||
44 | description: Whether to build examples. | ||
45 | default: False | ||
46 | |||
47 | flag network-uri | ||
48 | description: Use network-uri package. | ||
49 | default: True | ||
50 | |||
51 | flag bits-extras | ||
52 | description: Use more-effecient bits-extras bitwise operations. | ||
53 | default: False | ||
54 | |||
55 | flag dht-only | ||
56 | description: Build only DHT related modules. | ||
57 | default: True | ||
58 | |||
59 | flag builder | ||
60 | description: Use older bytestring package and bytestring-builder. | ||
61 | default: False | ||
62 | |||
63 | flag aeson | ||
64 | description: Use aeson for pretty-printing bencoded data. | ||
65 | default: True | ||
66 | |||
67 | flag thread-debug | ||
68 | description: Add instrumentation to threads. | ||
69 | default: True | ||
70 | |||
71 | library | ||
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 | |||
226 | test-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 | ||
333 | benchmark 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 | |||
345 | executable dht | ||
346 | hs-source-dirs: examples | ||
347 | main-is: dht.hs | ||
348 | default-language: Haskell2010 | ||
349 | build-depends: base, haskeline, network, bytestring, transformers | ||
350 | |||
351 | executable 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. | ||
369 | executable 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 | ||
401 | executable 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 @@ | |||
1 | This directory is for some dev scripts and other dev only stuff which | ||
2 | we don't want to keep in the resulting `cabal sdist` generated | ||
3 | tarball. Do _not_ include any of these files to .cabal file, neither | ||
4 | to `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 | |||
3 | for s in $(ls $(dirname $0)/../sub); do | ||
4 | (cd $(dirname $0)/.. && cabal sandbox add-source sub/$s) | ||
5 | done | ||
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 | ||
2 | cabal-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 | ||
2 | cabal-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 | |||
3 | cd $(dirname $0)/.. | ||
4 | |||
5 | git submodule init | ||
6 | git submodule foreach git fetch | ||
7 | git submodule update --recursive --checkout --force | ||
8 | |||
9 | $(dirname $0)/add-sources.sh | ||
10 | |||
11 | cabal 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 #-} | ||
5 | module Main (main) where | ||
6 | import Control.Concurrent | ||
7 | import Control.Monad.Trans | ||
8 | import Data.Maybe | ||
9 | import Options.Applicative | ||
10 | import System.Environment | ||
11 | import System.Exit | ||
12 | import System.IO | ||
13 | import Text.Read | ||
14 | |||
15 | import 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) | ||
20 | maybeReader f = eitherReader (maybe (Left ":(") Right . f) | ||
21 | #else | ||
22 | maybeReader f = f | ||
23 | #endif | ||
24 | |||
25 | {----------------------------------------------------------------------- | ||
26 | -- Command line arguments | ||
27 | -----------------------------------------------------------------------} | ||
28 | |||
29 | data TorrentBox = forall s. TorrentSource s => TorrentBox { unTorrentBox :: s } | ||
30 | |||
31 | data Args = Args | ||
32 | { topic :: TorrentBox | ||
33 | , contentDir :: FilePath | ||
34 | } | ||
35 | |||
36 | argsParser :: Parser Args | ||
37 | argsParser = 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 | |||
56 | argsInfo :: ParserInfo Args | ||
57 | argsInfo = info (helper <*> argsParser) | ||
58 | ( fullDesc | ||
59 | <> progDesc "A simple CLI bittorrent client" | ||
60 | <> header "foo" | ||
61 | ) | ||
62 | |||
63 | {----------------------------------------------------------------------- | ||
64 | -- Client | ||
65 | -----------------------------------------------------------------------} | ||
66 | |||
67 | run :: Args -> BitTorrent () | ||
68 | run (Args (TorrentBox t) dir) = do | ||
69 | h <- openHandle dir t | ||
70 | start h | ||
71 | liftIO $ threadDelay 10000000000 | ||
72 | |||
73 | main :: IO () | ||
74 | main = 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 #-} | ||
2 | module Main (main) where | ||
3 | |||
4 | import Control.Arrow | ||
5 | import Data.ByteString.Char8 as BC | ||
6 | import Data.List as L | ||
7 | import Data.Map as M | ||
8 | import Data.Torrent as T | ||
9 | import Data.Torrent.Tree as T | ||
10 | import System.Environment | ||
11 | import System.Fuse | ||
12 | import System.FilePath | ||
13 | import System.Posix.Files | ||
14 | |||
15 | |||
16 | defStat :: FileStat | ||
17 | defStat = 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 | |||
35 | dirStat :: FileStat | ||
36 | dirStat = defStat { | ||
37 | statEntryType = Directory | ||
38 | } | ||
39 | |||
40 | type Result a = IO (Either Errno a) | ||
41 | type Result' = IO Errno | ||
42 | |||
43 | fsGetFileStat :: Torrent -> FilePath -> Result FileStat | ||
44 | fsGetFileStat _ path = return $ Right dirStat | ||
45 | |||
46 | fsOpenDirectory :: Torrent -> FilePath -> Result' | ||
47 | fsOpenDirectory _ _ = return eOK | ||
48 | |||
49 | fsReadDirectory :: Torrent -> FilePath -> Result [(FilePath, FileStat)] | ||
50 | fsReadDirectory 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 | |||
57 | fsReleaseDirectory :: Torrent -> FilePath -> Result' | ||
58 | fsReleaseDirectory _ _ = return eOK | ||
59 | |||
60 | exfsOps :: Torrent -> FuseOperations () | ||
61 | exfsOps t = defaultFuseOps | ||
62 | { fuseGetFileStat = fsGetFileStat t | ||
63 | |||
64 | , fuseOpenDirectory = fsOpenDirectory t | ||
65 | , fuseReadDirectory = fsReadDirectory t | ||
66 | , fuseReleaseDirectory = fsReleaseDirectory t | ||
67 | } | ||
68 | |||
69 | main :: IO () | ||
70 | main = 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 #-} | ||
6 | module Main (main) where | ||
7 | |||
8 | import Prelude as P | ||
9 | import Control.Concurrent | ||
10 | import Control.Concurrent.Async.Lifted | ||
11 | import Control.Concurrent.ParallelIO | ||
12 | import Control.Exception | ||
13 | import Control.Lens hiding (argument, (<.>)) | ||
14 | import Control.Monad as M | ||
15 | import Control.Monad.Trans | ||
16 | import Data.Conduit as C | ||
17 | import Data.Conduit.List as C | ||
18 | import Data.List as L | ||
19 | import Data.Maybe as L | ||
20 | import Data.Monoid | ||
21 | import Data.Text as T | ||
22 | import qualified Data.Text.IO as T | ||
23 | import Data.Text.Read as T | ||
24 | import Data.Version | ||
25 | import Network | ||
26 | import Network.URI | ||
27 | import Options.Applicative | ||
28 | import System.Exit | ||
29 | import System.FilePath | ||
30 | import System.Log | ||
31 | import System.Log.Logger | ||
32 | import Text.Read | ||
33 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
34 | |||
35 | import Paths_bittorrent (version) | ||
36 | import Data.Torrent hiding (Magnet (Magnet)) | ||
37 | import Network.Address | ||
38 | import Network.BitTorrent.DHT.Session hiding (Options, options) | ||
39 | import Network.BitTorrent.DHT as DHT hiding (Options) | ||
40 | import Network.BitTorrent.Exchange.Bitfield as BF | ||
41 | import Network.BitTorrent.Exchange.Connection hiding (Options) | ||
42 | import Network.BitTorrent.Exchange.Message | ||
43 | import Network.BitTorrent.Exchange.Session | ||
44 | import 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) | ||
49 | maybeReader f = eitherReader (maybe (Left ":(") Right . f) | ||
50 | #else | ||
51 | maybeReader f = f | ||
52 | #endif | ||
53 | |||
54 | |||
55 | {----------------------------------------------------------------------- | ||
56 | -- Dialogs | ||
57 | -----------------------------------------------------------------------} | ||
58 | |||
59 | instance Read URI where | ||
60 | readsPrec _ = f . parseURI | ||
61 | where | ||
62 | f Nothing = [] | ||
63 | f (Just u) = [(u, "")] | ||
64 | |||
65 | question :: Show a => Text -> Maybe a -> IO () | ||
66 | question 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 | |||
72 | ask :: Read a => Text -> IO a | ||
73 | ask q = question q (Just True) >> getReply | ||
74 | where | ||
75 | getReply = do | ||
76 | resp <- P.getLine | ||
77 | maybe getReply return $ readMaybe resp | ||
78 | |||
79 | askMaybe :: Read a => Text -> IO (Maybe a) | ||
80 | askMaybe 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 | |||
88 | askURI :: IO URI | ||
89 | askURI = do | ||
90 | s <- P.getLine | ||
91 | case parseURI s of | ||
92 | Nothing -> T.putStrLn "incorrect URI" >> askURI | ||
93 | Just u -> return u | ||
94 | |||
95 | askFreeform :: IO Text | ||
96 | askFreeform = do | ||
97 | s <- T.getLine | ||
98 | if T.null s | ||
99 | then askFreeform | ||
100 | else return s | ||
101 | |||
102 | askInRange :: Int -> Int -> IO Int | ||
103 | askInRange 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 | |||
115 | askChoice :: [(Text, a)] -> IO a | ||
116 | askChoice 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 | |||
127 | torrentFile :: Parser FilePath | ||
128 | torrentFile = 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 | |||
137 | data AmendOpts = AmendOpts FilePath | ||
138 | deriving Show | ||
139 | |||
140 | amendInfo :: ParserInfo AmendOpts | ||
141 | amendInfo = info (helper <*> parser) modifier | ||
142 | where | ||
143 | modifier = progDesc "Edit info fields of existing torrent" | ||
144 | parser = AmendOpts <$> torrentFile | ||
145 | |||
146 | type Amend = Torrent -> Torrent | ||
147 | |||
148 | fields :: [(Text, IO Amend)] | ||
149 | fields = [ ("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 | |||
155 | askAmend :: IO Amend | ||
156 | askAmend = join $ T.putStrLn "Choose a field:" >> askChoice fields | ||
157 | |||
158 | amend :: AmendOpts -> IO () | ||
159 | amend (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 | |||
169 | data CheckOpts = CheckOpts | ||
170 | { checkTorrentPath :: FilePath -- ^ validation torrent file | ||
171 | , checkContentPath :: FilePath -- ^ root dir for content files | ||
172 | } deriving Show | ||
173 | |||
174 | checkInfo :: ParserInfo CheckOpts | ||
175 | checkInfo = 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 | |||
187 | validatePiece :: Storage -> PieceInfo -> PieceIx -> IO (Maybe PieceIx) | ||
188 | validatePiece 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 | |||
196 | validateStorage :: Storage -> PieceInfo -> IO Bitfield | ||
197 | validateStorage 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 | ||
205 | checkContent :: Storage -> PieceInfo -> IO () | ||
206 | checkContent 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 | |||
214 | checkTorrent :: CheckOpts -> IO () | ||
215 | checkTorrent 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 | {- | ||
237 | createFlags :: Parser CreateFlags | ||
238 | createFlags = 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 | |||
257 | createOpts :: Parser CreateOpts | ||
258 | createOpts = 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 | |||
269 | createInfo :: ParserInfo CreateOpts | ||
270 | createInfo = 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 | |||
279 | data MagnetOpts = MagnetOpts | ||
280 | { magnetFile :: FilePath -- ^ path to torrent file | ||
281 | , detailed :: Bool -- ^ whether to append additional uri params | ||
282 | } deriving Show | ||
283 | |||
284 | magnetInfo :: ParserInfo MagnetOpts | ||
285 | magnetInfo = info (helper <*> parser) modifier | ||
286 | where | ||
287 | modifier = progDesc "Print magnet link" | ||
288 | parser = MagnetOpts | ||
289 | <$> torrentFile | ||
290 | <*> switch ( long "detailed" ) | ||
291 | |||
292 | magnet :: MagnetOpts -> IO () | ||
293 | magnet 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 | |||
301 | data ShowOpts = ShowOpts | ||
302 | { showPath :: FilePath -- ^ torrent file to inspect; | ||
303 | , infoHashOnly :: Bool -- ^ omit everything except infohash. | ||
304 | } deriving Show | ||
305 | |||
306 | showInfo :: ParserInfo ShowOpts | ||
307 | showInfo = 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 | |||
317 | showTorrent :: ShowOpts -> Torrent -> ShowS | ||
318 | showTorrent ShowOpts {..} torrent | ||
319 | | infoHashOnly = shows $ idInfoHash (tInfoDict torrent) | ||
320 | | otherwise = shows $ pPrint torrent | ||
321 | |||
322 | putTorrent :: ShowOpts -> IO () | ||
323 | putTorrent 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 | |||
333 | data 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) | ||
341 | instance Read PortNumber where | ||
342 | readsPrec i s = [ (toEnum a, t) | (a, t) <- readsPrec i s] | ||
343 | #endif | ||
344 | |||
345 | paramsParser :: Parser GetOpts | ||
346 | paramsParser = 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 | |||
361 | getInfo :: ParserInfo GetOpts | ||
362 | getInfo = 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? | ||
369 | getTorrent :: GetOpts -> IO () | ||
370 | getTorrent 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 | |||
384 | data Command | ||
385 | = Amend AmendOpts | ||
386 | | Check CheckOpts | ||
387 | -- | Create CreateOpts | ||
388 | | Get GetOpts | ||
389 | | Magnet MagnetOpts | ||
390 | | Show ShowOpts | ||
391 | deriving Show | ||
392 | |||
393 | commandOpts :: Parser Command | ||
394 | commandOpts = 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 | |||
407 | data GlobalOpts = GlobalOpts | ||
408 | { verbosity :: Priority | ||
409 | } deriving Show | ||
410 | |||
411 | #if !MIN_VERSION_hslogger(1,2,9) | ||
412 | deriving instance Enum Priority | ||
413 | deriving instance Bounded Priority | ||
414 | #endif | ||
415 | |||
416 | priorities :: [Priority] | ||
417 | priorities = [minBound..maxBound] | ||
418 | |||
419 | defaultPriority :: Priority | ||
420 | defaultPriority = WARNING | ||
421 | |||
422 | verbosityOpts :: Parser Priority | ||
423 | verbosityOpts = 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 | |||
445 | globalOpts :: Parser GlobalOpts | ||
446 | globalOpts = GlobalOpts <$> verbosityOpts | ||
447 | |||
448 | data Options = Options | ||
449 | { cmdOpts :: Command | ||
450 | , globOpts :: GlobalOpts | ||
451 | } deriving Show | ||
452 | |||
453 | options :: Parser Options | ||
454 | options = Options <$> commandOpts <*> globalOpts | ||
455 | |||
456 | versioner :: String -> Version -> Parser (a -> a) | ||
457 | #if MIN_VERSION_optparse_applicative(0,10,0) | ||
458 | versioner prog ver = nullOption disabled $ mconcat | ||
459 | #else | ||
460 | versioner 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 | |||
472 | parserInfo :: ParserInfo Options | ||
473 | parserInfo = 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 | |||
484 | run :: Command -> IO () | ||
485 | run (Amend opts) = amend opts | ||
486 | run (Check opts) = checkTorrent opts | ||
487 | --run (Create opts) = createTorrent opts | ||
488 | run (Get opts) = getTorrent opts | ||
489 | run (Magnet opts) = magnet opts | ||
490 | run (Show opts) = putTorrent opts | ||
491 | |||
492 | prepare :: GlobalOpts -> IO () | ||
493 | prepare GlobalOpts {..} = do | ||
494 | updateGlobalLogger rootLoggerName (setLevel verbosity) | ||
495 | |||
496 | main :: IO () | ||
497 | main = 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 #-} | ||
9 | module 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 | |||
59 | import Data.Torrent | ||
60 | import Network.BitTorrent.Client | ||
61 | import 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 #-} | ||
5 | module 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 | |||
50 | import Control.Applicative | ||
51 | import Control.Exception | ||
52 | import Control.Concurrent | ||
53 | import Control.Concurrent.Chan.Split as CS | ||
54 | import Control.Monad.Logger | ||
55 | import Control.Monad.Trans | ||
56 | import Control.Monad.Trans.Resource | ||
57 | |||
58 | import Data.Default | ||
59 | import Data.HashMap.Strict as HM | ||
60 | import Data.Text | ||
61 | import Network | ||
62 | |||
63 | import Data.Torrent | ||
64 | import Network.Address | ||
65 | import Network.BitTorrent.Client.Types | ||
66 | import Network.BitTorrent.Client.Handle | ||
67 | import Network.BitTorrent.DHT as DHT hiding (Options) | ||
68 | import Network.BitTorrent.Tracker as Tracker hiding (Options) | ||
69 | import Network.BitTorrent.Exchange as Exchange hiding (Options) | ||
70 | import qualified Network.BitTorrent.Exchange as Exchange (Options(..)) | ||
71 | |||
72 | |||
73 | data 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 | |||
82 | instance 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 | |||
92 | exchangeOptions :: PeerId -> Options -> Exchange.Options | ||
93 | exchangeOptions pid Options {..} = Exchange.Options | ||
94 | { optPeerAddr = PeerAddr (Just pid) (peerHost def) optPort | ||
95 | , optBacklog = optBacklog def | ||
96 | } | ||
97 | |||
98 | connHandler :: MVar (HashMap InfoHash Handle) -> Exchange.Handler | ||
99 | connHandler 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 | |||
105 | initClient :: Options -> LogFun -> ResIO Client | ||
106 | initClient 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 | |||
136 | newClient :: Options -> LogFun -> IO Client | ||
137 | newClient opts logFun = do | ||
138 | s <- createInternalState | ||
139 | runInternalState (initClient opts logFun) s | ||
140 | `onException` closeInternalState s | ||
141 | |||
142 | closeClient :: Client -> IO () | ||
143 | closeClient Client {..} = closeInternalState clientResources | ||
144 | |||
145 | withClient :: Options -> LogFun -> (Client -> IO a) -> IO a | ||
146 | withClient 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 | -- | ||
158 | simpleClient :: BitTorrent () -> IO () | ||
159 | simpleClient m = do | ||
160 | runStderrLoggingT $ LoggingT $ \ logger -> do | ||
161 | withClient def logger (`runBitTorrent` m) | ||
162 | |||
163 | {----------------------------------------------------------------------- | ||
164 | -- Torrent identifiers | ||
165 | -----------------------------------------------------------------------} | ||
166 | |||
167 | class TorrentSource s where | ||
168 | openHandle :: FilePath -> s -> BitTorrent Handle | ||
169 | |||
170 | instance TorrentSource InfoHash where | ||
171 | openHandle path ih = openMagnet path (nullMagnet ih) | ||
172 | {-# INLINE openHandle #-} | ||
173 | |||
174 | instance TorrentSource Magnet where | ||
175 | openHandle = openMagnet | ||
176 | {-# INLINE openHandle #-} | ||
177 | |||
178 | instance TorrentSource InfoDict where | ||
179 | openHandle path dict = openTorrent path (nullTorrent dict) | ||
180 | {-# INLINE openHandle #-} | ||
181 | |||
182 | instance TorrentSource Torrent where | ||
183 | openHandle = openTorrent | ||
184 | {-# INLINE openHandle #-} | ||
185 | |||
186 | instance TorrentSource FilePath where | ||
187 | openHandle contentDir torrentPath = do | ||
188 | t <- liftIO $ fromFile torrentPath | ||
189 | openTorrent contentDir t | ||
190 | {-# INLINE openHandle #-} | ||
191 | |||
192 | getIndex :: BitTorrent [Handle] | ||
193 | getIndex = 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 @@ | |||
1 | module 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 | |||
20 | import Control.Concurrent.Chan.Split | ||
21 | import Control.Concurrent.Lifted as L | ||
22 | import Control.Monad | ||
23 | import Control.Monad.Trans | ||
24 | import Data.Default | ||
25 | import Data.List as L | ||
26 | import Data.HashMap.Strict as HM | ||
27 | |||
28 | import Data.Torrent | ||
29 | import Network.BitTorrent.Client.Types as Types | ||
30 | import Network.BitTorrent.DHT as DHT | ||
31 | import Network.BitTorrent.Exchange as Exchange | ||
32 | import Network.BitTorrent.Tracker as Tracker | ||
33 | |||
34 | {----------------------------------------------------------------------- | ||
35 | -- Safe handle set manupulation | ||
36 | -----------------------------------------------------------------------} | ||
37 | |||
38 | allocHandle :: InfoHash -> BitTorrent Handle -> BitTorrent Handle | ||
39 | allocHandle 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 | |||
54 | freeHandle :: InfoHash -> BitTorrent () -> BitTorrent () | ||
55 | freeHandle 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 | |||
65 | lookupHandle :: InfoHash -> BitTorrent (Maybe Handle) | ||
66 | lookupHandle ih = do | ||
67 | Client {..} <- getClient | ||
68 | handles <- readMVar clientTorrents | ||
69 | return (HM.lookup ih handles) | ||
70 | |||
71 | {----------------------------------------------------------------------- | ||
72 | -- Initialization | ||
73 | -----------------------------------------------------------------------} | ||
74 | |||
75 | newExchangeSession :: FilePath -> Either InfoHash InfoDict -> BitTorrent Exchange.Session | ||
76 | newExchangeSession 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. | ||
82 | openTorrent :: FilePath -> Torrent -> BitTorrent Handle | ||
83 | openTorrent 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'. | ||
100 | openMagnet :: FilePath -> Magnet -> BitTorrent Handle | ||
101 | openMagnet 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. | ||
120 | closeHandle :: Handle -> BitTorrent () | ||
121 | closeHandle 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 | |||
132 | modifyStatus :: HandleStatus -> Handle -> (HandleStatus -> BitTorrent ()) -> BitTorrent () | ||
133 | modifyStatus 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. | ||
144 | start :: Handle -> BitTorrent () | ||
145 | start 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. | ||
161 | pause :: Handle -> BitTorrent () | ||
162 | pause _ = return () | ||
163 | |||
164 | -- | Stop downloading, uploading and announcing this torrent. | ||
165 | stop :: Handle -> BitTorrent () | ||
166 | stop 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 | |||
180 | getHandle :: InfoHash -> BitTorrent Handle | ||
181 | getHandle ih = do | ||
182 | mhandle <- lookupHandle ih | ||
183 | case mhandle of | ||
184 | Nothing -> error "should we throw some exception?" | ||
185 | Just h -> return h | ||
186 | |||
187 | getStatus :: Handle -> IO HandleStatus | ||
188 | getStatus 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 #-} | ||
6 | module 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 | |||
24 | import Control.Applicative | ||
25 | import Control.Concurrent | ||
26 | import Control.Concurrent.Chan.Split as CS | ||
27 | import Control.Monad.Base | ||
28 | import Control.Monad.Logger | ||
29 | import Control.Monad.Reader | ||
30 | import Control.Monad.Trans.Control | ||
31 | import Control.Monad.Trans.Resource | ||
32 | import Data.Function | ||
33 | import Data.HashMap.Strict as HM | ||
34 | import Data.Ord | ||
35 | import Network | ||
36 | import System.Log.FastLogger | ||
37 | |||
38 | import Data.Torrent | ||
39 | import Network.Address | ||
40 | import Network.BitTorrent.Internal.Types as Types | ||
41 | import Network.BitTorrent.DHT as DHT | ||
42 | import Network.BitTorrent.Exchange as Exchange | ||
43 | import Network.BitTorrent.Tracker as Tracker hiding (Event) | ||
44 | |||
45 | data HandleStatus | ||
46 | = Running | ||
47 | | Stopped | ||
48 | deriving (Show, Eq) | ||
49 | |||
50 | data 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 | |||
60 | instance EventSource Handle where | ||
61 | data Event Handle = StatusChanged HandleStatus | ||
62 | listen Handle {..} = CS.listen undefined | ||
63 | |||
64 | data 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 | |||
77 | instance Eq Client where | ||
78 | (==) = (==) `on` clientPeerId | ||
79 | |||
80 | instance Ord Client where | ||
81 | compare = comparing clientPeerId | ||
82 | |||
83 | instance 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'. | ||
90 | externalAddr :: Client -> PeerAddr (Maybe IP) | ||
91 | externalAddr 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 | |||
101 | newtype BitTorrent a = BitTorrent | ||
102 | { unBitTorrent :: ReaderT Client IO a | ||
103 | } deriving ( Functor, Applicative, Monad | ||
104 | , MonadIO, MonadThrow, MonadBase IO | ||
105 | ) | ||
106 | |||
107 | class MonadBitTorrent m where | ||
108 | liftBT :: BitTorrent a -> m a | ||
109 | |||
110 | #if MIN_VERSION_monad_control(1,0,0) | ||
111 | newtype BTStM a = BTStM { unBTSt :: StM (ReaderT Client IO) a } | ||
112 | |||
113 | instance 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 | ||
122 | instance 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. | ||
133 | instance MonadBitTorrent BitTorrent where | ||
134 | liftBT = id | ||
135 | |||
136 | instance 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' | ||
141 | instance 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. | ||
147 | instance MonadDHT BitTorrent where | ||
148 | liftDHT action = BitTorrent $ do | ||
149 | node <- asks clientNode | ||
150 | liftIO $ runDHT node action | ||
151 | |||
152 | instance MonadLogger BitTorrent where | ||
153 | monadLoggerLog loc src lvl msg = BitTorrent $ do | ||
154 | logger <- asks clientLogger | ||
155 | liftIO $ logger loc src lvl (toLogStr msg) | ||
156 | |||
157 | runBitTorrent :: Client -> BitTorrent a -> IO a | ||
158 | runBitTorrent client action = runReaderT (unBitTorrent action) client | ||
159 | {-# INLINE runBitTorrent #-} | ||
160 | |||
161 | getClient :: BitTorrent Client | ||
162 | getClient = 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 | -- | ||
8 | module 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 | |||
33 | import Network.BitTorrent.Exchange.Manager | ||
34 | import Network.BitTorrent.Exchange.Message | ||
35 | import 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 #-} | ||
31 | module 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 | |||
92 | import Control.Monad | ||
93 | import Control.Monad.ST | ||
94 | import Data.ByteString (ByteString) | ||
95 | import qualified Data.ByteString as B | ||
96 | import qualified Data.ByteString.Lazy as Lazy | ||
97 | import Data.Vector.Unboxed (Vector) | ||
98 | import qualified Data.Vector.Unboxed as V | ||
99 | import qualified Data.Vector.Unboxed.Mutable as VM | ||
100 | import Data.IntervalSet (IntSet) | ||
101 | import qualified Data.IntervalSet as S | ||
102 | import qualified Data.IntervalSet.ByteString as S | ||
103 | import Data.List (foldl') | ||
104 | import Data.Monoid | ||
105 | import Data.Ratio | ||
106 | |||
107 | import 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 | -- | ||
117 | data 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 | |||
124 | instance 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. | ||
135 | haveNone :: PieceCount -> Bitfield | ||
136 | haveNone s = Bitfield s S.empty | ||
137 | |||
138 | -- | The full bitfield containing all piece indices for the given size. | ||
139 | haveAll :: PieceCount -> Bitfield | ||
140 | haveAll s = Bitfield s (S.interval 0 (s - 1)) | ||
141 | |||
142 | -- | Insert the index in the set ignoring out of range indices. | ||
143 | have :: PieceIx -> Bitfield -> Bitfield | ||
144 | have ix Bitfield {..} | ||
145 | | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet) | ||
146 | | otherwise = Bitfield bfSize bfSet | ||
147 | |||
148 | singleton :: PieceIx -> PieceCount -> Bitfield | ||
149 | singleton ix pc = have ix (haveNone pc) | ||
150 | |||
151 | -- | Assign new size to bitfield. FIXME Normally, size should be only | ||
152 | -- decreased, otherwise exception raised. | ||
153 | adjustSize :: PieceCount -> Bitfield -> Bitfield | ||
154 | adjustSize s Bitfield {..} = Bitfield s bfSet | ||
155 | |||
156 | -- | NOTE: for internal use only | ||
157 | interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield | ||
158 | interval 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. | ||
165 | null :: Bitfield -> Bool | ||
166 | null Bitfield {..} = S.null bfSet | ||
167 | |||
168 | -- | Test if bitfield have all pieces. | ||
169 | full :: Bitfield -> Bool | ||
170 | full Bitfield {..} = S.size bfSet == bfSize | ||
171 | |||
172 | -- | Count of peer have pieces. | ||
173 | haveCount :: Bitfield -> PieceCount | ||
174 | haveCount = S.size . bfSet | ||
175 | |||
176 | -- | Total count of pieces and its indices. | ||
177 | totalCount :: Bitfield -> PieceCount | ||
178 | totalCount = bfSize | ||
179 | |||
180 | -- | Ratio of /have/ piece count to the /total/ piece count. | ||
181 | -- | ||
182 | -- > forall bf. 0 <= completeness bf <= 1 | ||
183 | -- | ||
184 | completeness :: Bitfield -> Ratio PieceCount | ||
185 | completeness b = haveCount b % totalCount b | ||
186 | |||
187 | inRange :: PieceIx -> Bitfield -> Bool | ||
188 | inRange ix Bitfield {..} = 0 <= ix && ix < bfSize | ||
189 | |||
190 | member :: PieceIx -> Bitfield -> Bool | ||
191 | member ix bf @ Bitfield {..} | ||
192 | | ix `inRange` bf = ix `S.member` bfSet | ||
193 | | otherwise = False | ||
194 | |||
195 | notMember :: PieceIx -> Bitfield -> Bool | ||
196 | notMember ix bf @ Bitfield {..} | ||
197 | | ix `inRange` bf = ix `S.notMember` bfSet | ||
198 | | otherwise = True | ||
199 | |||
200 | -- | Find first available piece index. | ||
201 | findMin :: Bitfield -> PieceIx | ||
202 | findMin = S.findMin . bfSet | ||
203 | {-# INLINE findMin #-} | ||
204 | |||
205 | -- | Find last available piece index. | ||
206 | findMax :: Bitfield -> PieceIx | ||
207 | findMax = S.findMax . bfSet | ||
208 | {-# INLINE findMax #-} | ||
209 | |||
210 | -- | Check if all pieces from first bitfield present if the second bitfield | ||
211 | isSubsetOf :: Bitfield -> Bitfield -> Bool | ||
212 | isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b | ||
213 | {-# INLINE isSubsetOf #-} | ||
214 | |||
215 | -- | Resulting bitfield includes only missing pieces. | ||
216 | complement :: Bitfield -> Bitfield | ||
217 | complement 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. | ||
232 | type Frequency = Int | ||
233 | |||
234 | -- TODO rename to availability | ||
235 | -- | How many times each piece index occur in the given bitfield set. | ||
236 | frequencies :: [Bitfield] -> Vector Frequency | ||
237 | frequencies [] = V.fromList [] | ||
238 | frequencies 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'. | ||
253 | rarest :: [Bitfield] -> Maybe PieceIx | ||
254 | rarest 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 | |||
273 | insert :: PieceIx -> Bitfield -> Bitfield | ||
274 | insert 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. | ||
282 | union :: Bitfield -> Bitfield -> Bitfield | ||
283 | union 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. | ||
289 | intersection :: Bitfield -> Bitfield -> Bitfield | ||
290 | intersection 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. | ||
296 | difference :: Bitfield -> Bitfield -> Bitfield | ||
297 | difference 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. | ||
303 | unions :: [Bitfield] -> Bitfield | ||
304 | unions = {-# SCC unions #-} foldl' union (haveNone 0) | ||
305 | |||
306 | {----------------------------------------------------------------------- | ||
307 | Serialization | ||
308 | -----------------------------------------------------------------------} | ||
309 | |||
310 | -- | List all /have/ indexes. | ||
311 | toList :: Bitfield -> [PieceIx] | ||
312 | toList Bitfield {..} = S.toList bfSet | ||
313 | |||
314 | -- | Make bitfield from list of /have/ indexes. | ||
315 | fromList :: PieceCount -> [PieceIx] -> Bitfield | ||
316 | fromList 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'. | ||
323 | fromBitmap :: ByteString -> Bitfield | ||
324 | fromBitmap 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. | ||
331 | toBitmap :: Bitfield -> Lazy.ByteString | ||
332 | toBitmap 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 | |||
342 | type 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 | |||
348 | selector :: 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. | ||
354 | selector 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 | |||
360 | data StartegyClass | ||
361 | = SCBeginning | ||
362 | | SCReady | ||
363 | | SCEnd | ||
364 | deriving (Show, Eq, Ord, Enum, Bounded) | ||
365 | |||
366 | |||
367 | strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass | ||
368 | strategyClass 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. | ||
378 | strictFirst :: Selector | ||
379 | strictFirst h a _ = Just $ findMin (difference a h) | ||
380 | |||
381 | -- | Select the last available piece. | ||
382 | strictLast :: Selector | ||
383 | strictLast h a _ = Just $ findMax (difference a h) | ||
384 | |||
385 | -- | | ||
386 | rarestFirst :: Selector | ||
387 | rarestFirst 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. | ||
393 | randomFirst :: Selector | ||
394 | randomFirst = do | ||
395 | -- randomIO | ||
396 | error "randomFirst" | ||
397 | |||
398 | endGame :: Selector | ||
399 | endGame = 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 #-} | ||
16 | module 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 | |||
58 | import Prelude hiding (span) | ||
59 | import Control.Applicative | ||
60 | import Data.ByteString as BS hiding (span) | ||
61 | import Data.ByteString.Lazy as BL hiding (span) | ||
62 | import Data.ByteString.Lazy.Builder as BS | ||
63 | import Data.Default | ||
64 | import Data.Monoid | ||
65 | import Data.List as L hiding (span) | ||
66 | import Data.Serialize as S | ||
67 | import Data.Typeable | ||
68 | import Numeric | ||
69 | import Text.PrettyPrint as PP hiding ((<>)) | ||
70 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
71 | |||
72 | import 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. | ||
80 | type 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 | -- | ||
86 | type BlockSize = Int | ||
87 | |||
88 | -- | Number of block in a piece of a torrent. Used to distinguish | ||
89 | -- block count from piece count. | ||
90 | type 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 | -- | ||
96 | defaultTransferSize :: BlockSize | ||
97 | defaultTransferSize = 16 * 1024 | ||
98 | |||
99 | {----------------------------------------------------------------------- | ||
100 | Block Index | ||
101 | -----------------------------------------------------------------------} | ||
102 | |||
103 | -- | BlockIx correspond. | ||
104 | data 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. | ||
116 | instance Default BlockIx where | ||
117 | def = BlockIx 0 0 defaultTransferSize | ||
118 | |||
119 | getInt :: S.Get Int | ||
120 | getInt = fromIntegral <$> S.getWord32be | ||
121 | {-# INLINE getInt #-} | ||
122 | |||
123 | putInt :: S.Putter Int | ||
124 | putInt = S.putWord32be . fromIntegral | ||
125 | {-# INLINE putInt #-} | ||
126 | |||
127 | instance 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 | |||
140 | instance 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. | ||
147 | blockIxRange :: (Num a, Integral a) => PieceSize -> BlockIx -> (a, a) | ||
148 | blockIxRange 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 | |||
159 | data 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. | ||
171 | instance Pretty (Block BL.ByteString) where | ||
172 | pPrint = pPrint . blockIx | ||
173 | {-# INLINE pPrint #-} | ||
174 | |||
175 | -- | Get size of block /payload/ in bytes. | ||
176 | blockSize :: Block BL.ByteString -> BlockSize | ||
177 | blockSize = fromIntegral . BL.length . blkData | ||
178 | {-# INLINE blockSize #-} | ||
179 | |||
180 | -- | Get block index of a block. | ||
181 | blockIx :: Block BL.ByteString -> BlockIx | ||
182 | blockIx = BlockIx <$> blkPiece <*> blkOffset <*> blockSize | ||
183 | |||
184 | -- | Get location of payload bytes in the torrent content. | ||
185 | blockRange :: (Num a, Integral a) | ||
186 | => PieceSize -> Block BL.ByteString -> (a, a) | ||
187 | blockRange piSize = blockIxRange piSize . blockIx | ||
188 | {-# INLINE blockRange #-} | ||
189 | |||
190 | -- | Test if a block can be safely turned into a piece. | ||
191 | isPiece :: PieceSize -> Block BL.ByteString -> Bool | ||
192 | isPiece pieceLen blk @ (Block i offset _) = | ||
193 | offset == 0 && blockSize blk == pieceLen && i >= 0 | ||
194 | {-# INLINE isPiece #-} | ||
195 | |||
196 | -- | First block in the piece. | ||
197 | leadingBlock :: PieceIx -> BlockSize -> BlockIx | ||
198 | leadingBlock pix blockSize = BlockIx | ||
199 | { ixPiece = pix | ||
200 | , ixOffset = 0 | ||
201 | , ixLength = blockSize | ||
202 | } | ||
203 | {-# INLINE leadingBlock #-} | ||
204 | |||
205 | {----------------------------------------------------------------------- | ||
206 | -- Bucket | ||
207 | -----------------------------------------------------------------------} | ||
208 | |||
209 | type Pos = Int | ||
210 | type ChunkSize = Int | ||
211 | |||
212 | -- | A sparse set of blocks used to represent an /in progress/ piece. | ||
213 | data Bucket | ||
214 | = Nil | ||
215 | | Span {-# UNPACK #-} !ChunkSize !Bucket | ||
216 | | Fill {-# UNPACK #-} !ChunkSize !Builder !Bucket | ||
217 | |||
218 | instance 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'. | ||
226 | nilInvFailed :: a | ||
227 | nilInvFailed = error "Nil: bucket invariant failed" | ||
228 | |||
229 | valid :: Bucket -> Bool | ||
230 | valid = 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 | |||
243 | instance 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. | ||
253 | span :: ChunkSize -> Bucket -> Bucket | ||
254 | span sz (Span sz' xs) = Span (sz + sz') xs | ||
255 | span sz xxs = Span sz xxs | ||
256 | {-# INLINE span #-} | ||
257 | |||
258 | -- | Smart constructor: use it when some block is /inserted/ to | ||
259 | -- bucket. | ||
260 | fill :: ChunkSize -> Builder -> Bucket -> Bucket | ||
261 | fill sz b (Fill sz' b' xs) = Fill (sz + sz') (b <> b') xs | ||
262 | fill 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. | ||
270 | null :: Bucket -> Bool | ||
271 | null Nil = nilInvFailed | ||
272 | null (Span _ Nil) = True | ||
273 | null _ = False | ||
274 | {-# INLINE null #-} | ||
275 | |||
276 | -- | /O(1)/. Test if this bucket is complete. | ||
277 | full :: Bucket -> Bool | ||
278 | full Nil = nilInvFailed | ||
279 | full (Fill _ _ Nil) = True | ||
280 | full _ = False | ||
281 | {-# INLINE full #-} | ||
282 | |||
283 | -- | /O(n)/. Total size of the incompleted piece. | ||
284 | size :: Bucket -> PieceSize | ||
285 | size Nil = nilInvFailed | ||
286 | size 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. | ||
296 | spans :: BlockSize -> Bucket -> [(BlockOffset, BlockSize)] | ||
297 | spans 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. | ||
315 | empty :: PieceSize -> Bucket | ||
316 | empty sz | ||
317 | | sz < 0 = error "empty: Bucket size must be a non-negative value" | ||
318 | | otherwise = Span sz Nil | ||
319 | {-# INLINE empty #-} | ||
320 | |||
321 | insertSpan :: Pos -> BS.ByteString -> ChunkSize -> Bucket -> Bucket | ||
322 | insertSpan !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 | -- | ||
339 | insert :: Pos -> BS.ByteString -> Bucket -> Bucket | ||
340 | insert _ _ Nil = nilInvFailed | ||
341 | insert 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 | |||
353 | fromList :: PieceSize -> [(Pos, BS.ByteString)] -> Bucket | ||
354 | fromList s = L.foldr (uncurry Network.BitTorrent.Exchange.Block.insert) | ||
355 | (Network.BitTorrent.Exchange.Block.empty s) | ||
356 | |||
357 | -- TODO zero-copy | ||
358 | insertLazy :: Pos -> BL.ByteString -> Bucket -> Bucket | ||
359 | insertLazy pos bl = Network.BitTorrent.Exchange.Block.insert pos (BL.toStrict bl) | ||
360 | |||
361 | -- | /O(n)/. | ||
362 | merge :: Bucket -> Bucket -> Bucket | ||
363 | merge = error "Bucket.merge: not implemented" | ||
364 | |||
365 | -- | /O(1)/. | ||
366 | toPiece :: Bucket -> Maybe BL.ByteString | ||
367 | toPiece Nil = nilInvFailed | ||
368 | toPiece (Fill _ b Nil) = Just (toLazyByteString b) | ||
369 | toPiece _ = 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 #-} | ||
21 | module 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 | |||
110 | import Control.Applicative | ||
111 | import Control.Concurrent hiding (yield) | ||
112 | import Control.Exception | ||
113 | import Control.Monad.Reader | ||
114 | import Control.Monad.State | ||
115 | import Control.Monad.Trans.Resource | ||
116 | import Control.Lens | ||
117 | import Data.ByteString as BS | ||
118 | import Data.ByteString.Lazy as BSL | ||
119 | import Data.Conduit as C | ||
120 | import Data.Conduit.Cereal | ||
121 | import Data.Conduit.List | ||
122 | import Data.Conduit.Network | ||
123 | import Data.Default | ||
124 | import Data.IORef | ||
125 | import Data.List as L | ||
126 | import Data.Maybe as M | ||
127 | import Data.Monoid | ||
128 | import Data.Serialize as S | ||
129 | import Data.Typeable | ||
130 | import Network | ||
131 | import Network.Socket hiding (Connected) | ||
132 | import Network.Socket.ByteString as BS | ||
133 | import Text.PrettyPrint as PP hiding ((<>)) | ||
134 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
135 | import Text.Show.Functions () | ||
136 | import System.Log.FastLogger (ToLogStr(..)) | ||
137 | import System.Timeout | ||
138 | |||
139 | import Data.Torrent | ||
140 | import Network.Address | ||
141 | import Network.BitTorrent.Exchange.Bitfield as BF | ||
142 | import 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'. | ||
155 | data ChannelSide | ||
156 | = ThisPeer | ||
157 | | RemotePeer | ||
158 | deriving (Show, Eq, Enum, Bounded) | ||
159 | |||
160 | instance Default ChannelSide where | ||
161 | def = ThisPeer | ||
162 | |||
163 | instance Pretty ChannelSide where | ||
164 | pPrint = PP.text . show | ||
165 | |||
166 | -- | A protocol errors occur when a peer violates protocol | ||
167 | -- specification. | ||
168 | data 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 | |||
215 | instance Pretty ProtocolError where | ||
216 | pPrint = PP.text . show | ||
217 | |||
218 | errorPenalty :: ProtocolError -> Int | ||
219 | errorPenalty (InvalidProtocol _) = 1 | ||
220 | errorPenalty (UnexpectedProtocol _) = 1 | ||
221 | errorPenalty (UnexpectedTopic _) = 1 | ||
222 | errorPenalty (UnexpectedPeerId _) = 1 | ||
223 | errorPenalty (UnknownTopic _) = 0 | ||
224 | errorPenalty (HandshakeRefused ) = 1 | ||
225 | errorPenalty (BitfieldAlreadySent _) = 1 | ||
226 | errorPenalty (DisallowedMessage _ _) = 1 | ||
227 | |||
228 | -- | Exceptions used to interrupt the current P2P session. | ||
229 | data 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 | |||
256 | instance Exception WireFailure | ||
257 | |||
258 | instance Pretty WireFailure where | ||
259 | pPrint = PP.text . show | ||
260 | |||
261 | -- TODO | ||
262 | -- data Penalty = Ban | Penalty Int | ||
263 | |||
264 | peerPenalty :: WireFailure -> Int | ||
265 | peerPenalty DisconnectPeer = 0 | ||
266 | peerPenalty PeerDisconnected = 0 | ||
267 | peerPenalty (DecodingError _) = 1 | ||
268 | peerPenalty (ProtocolError e) = errorPenalty e | ||
269 | peerPenalty (FloodDetected _) = 1 | ||
270 | |||
271 | -- | Do nothing with exception, used with 'handle' or 'try'. | ||
272 | isWireFailure :: Monad m => WireFailure -> m () | ||
273 | isWireFailure _ = return () | ||
274 | |||
275 | protocolError :: MonadThrow m => ProtocolError -> m a | ||
276 | protocolError = monadThrow . ProtocolError | ||
277 | |||
278 | {----------------------------------------------------------------------- | ||
279 | -- Stats | ||
280 | -----------------------------------------------------------------------} | ||
281 | |||
282 | -- | Message stats in one direction. | ||
283 | data 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 | |||
290 | instance Pretty FlowStats where | ||
291 | pPrint FlowStats {..} = | ||
292 | PP.int messageCount <+> "messages" $+$ | ||
293 | pPrint messageBytes | ||
294 | |||
295 | -- | Zeroed stats. | ||
296 | instance Default FlowStats where | ||
297 | def = FlowStats 0 def | ||
298 | |||
299 | -- | Monoid under addition. | ||
300 | instance 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. | ||
308 | avgByteStats :: FlowStats -> ByteStats | ||
309 | avgByteStats (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 | -- | ||
323 | data ConnectionStats = ConnectionStats | ||
324 | { -- | Received messages stats. | ||
325 | incomingFlow :: !FlowStats | ||
326 | -- | Sent messages stats. | ||
327 | , outcomingFlow :: !FlowStats | ||
328 | } deriving Show | ||
329 | |||
330 | instance 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. | ||
338 | instance Default ConnectionStats where | ||
339 | def = ConnectionStats def def | ||
340 | |||
341 | -- | Monoid under addition. | ||
342 | instance 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. | ||
350 | addStats :: ChannelSide -> ByteStats -> ConnectionStats -> ConnectionStats | ||
351 | addStats ThisPeer x s = s { outcomingFlow = (FlowStats 1 x) <> (outcomingFlow s) } | ||
352 | addStats RemotePeer x s = s { incomingFlow = (FlowStats 1 x) <> (incomingFlow s) } | ||
353 | |||
354 | -- | Sum of overhead and control bytes in both directions. | ||
355 | wastedBytes :: ConnectionStats -> Int | ||
356 | wastedBytes ConnectionStats {..} = overhead + control | ||
357 | where | ||
358 | FlowStats _ ByteStats {..} = incomingFlow <> outcomingFlow | ||
359 | |||
360 | -- | Sum of payload bytes in both directions. | ||
361 | payloadBytes :: ConnectionStats -> Int | ||
362 | payloadBytes ConnectionStats {..} = | ||
363 | payload (messageBytes (incomingFlow <> outcomingFlow)) | ||
364 | |||
365 | -- | Sum of any bytes in both directions. | ||
366 | transmittedBytes :: ConnectionStats -> Int | ||
367 | transmittedBytes ConnectionStats {..} = | ||
368 | byteLength (messageBytes (incomingFlow <> outcomingFlow)) | ||
369 | |||
370 | {----------------------------------------------------------------------- | ||
371 | -- Flood protection | ||
372 | -----------------------------------------------------------------------} | ||
373 | |||
374 | defaultFloodFactor :: Int | ||
375 | defaultFloodFactor = 1 | ||
376 | |||
377 | -- | This is a very permissive value, connection setup usually takes | ||
378 | -- around 10-100KB, including both directions. | ||
379 | defaultFloodThreshold :: Int | ||
380 | defaultFloodThreshold = 2 * 1024 * 1024 | ||
381 | |||
382 | -- | A flood detection function. | ||
383 | type Detector stats = Int -- ^ Factor; | ||
384 | -> Int -- ^ Threshold; | ||
385 | -> stats -- ^ Stats to analyse; | ||
386 | -> Bool -- ^ Is this a flooded connection? | ||
387 | |||
388 | defaultDetector :: Detector ConnectionStats | ||
389 | defaultDetector 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. | ||
395 | data 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 | |||
409 | instance Eq FloodDetector where | ||
410 | a == b = floodFactor a == floodFactor b | ||
411 | && floodThreshold a == floodThreshold b | ||
412 | |||
413 | -- | Flood detector with very permissive options. | ||
414 | instance 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. | ||
422 | runDetector :: FloodDetector -> ConnectionStats -> Bool | ||
423 | runDetector FloodDetector {..} = floodPredicate floodFactor floodThreshold | ||
424 | |||
425 | {----------------------------------------------------------------------- | ||
426 | -- Options | ||
427 | -----------------------------------------------------------------------} | ||
428 | |||
429 | -- | Various connection settings and limits. | ||
430 | data 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. | ||
468 | instance 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. | ||
484 | data 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 | |||
495 | instance Pretty PeerStatus where | ||
496 | pPrint PeerStatus {..} = | ||
497 | pPrint (Choking _choking) <+> "and" <+> pPrint (Interested _interested) | ||
498 | |||
499 | -- | Connections start out choked and not interested. | ||
500 | instance Default PeerStatus where | ||
501 | def = PeerStatus True False | ||
502 | |||
503 | instance 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. | ||
512 | updateStatus :: StatusUpdate -> PeerStatus -> PeerStatus | ||
513 | updateStatus (Choking b) = choking .~ b | ||
514 | updateStatus (Interested b) = interested .~ b | ||
515 | |||
516 | -- | Can be used to generate outcoming messages. | ||
517 | statusUpdates :: PeerStatus -> PeerStatus -> [StatusUpdate] | ||
518 | statusUpdates 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. | ||
530 | data ConnectionStatus = ConnectionStatus | ||
531 | { _clientStatus :: !PeerStatus | ||
532 | , _remoteStatus :: !PeerStatus | ||
533 | } deriving (Show, Eq) | ||
534 | |||
535 | $(makeLenses ''ConnectionStatus) | ||
536 | |||
537 | instance 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. | ||
543 | instance Default ConnectionStatus where | ||
544 | def = ConnectionStatus def def | ||
545 | |||
546 | -- | Can the client transfer to the remote peer? | ||
547 | canUpload :: ConnectionStatus -> Bool | ||
548 | canUpload ConnectionStatus {..} | ||
549 | = _interested _remoteStatus && not (_choking _clientStatus) | ||
550 | |||
551 | -- | Can the client transfer from the remote peer? | ||
552 | canDownload :: ConnectionStatus -> Bool | ||
553 | canDownload ConnectionStatus {..} | ||
554 | = _interested _clientStatus && not (_choking _remoteStatus) | ||
555 | |||
556 | -- | Indicates how many peers are allowed to download from the client | ||
557 | -- by default. | ||
558 | defaultUnchokeSlots :: Int | ||
559 | defaultUnchokeSlots = 4 | ||
560 | |||
561 | -- | | ||
562 | defaultRechokeInterval :: Int | ||
563 | defaultRechokeInterval = 10 * 1000 * 1000 | ||
564 | |||
565 | {----------------------------------------------------------------------- | ||
566 | -- Connection | ||
567 | -----------------------------------------------------------------------} | ||
568 | |||
569 | data 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 | |||
595 | makeLenses ''ConnectionState | ||
596 | |||
597 | instance 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. | ||
607 | data 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 | |||
648 | instance Pretty (Connection s) where | ||
649 | pPrint Connection {..} = "Connection" | ||
650 | |||
651 | instance 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 | ||
663 | isAllowed :: Connection s -> Message -> Bool | ||
664 | isAllowed Connection {..} msg | ||
665 | | Just ext <- requires msg = ext `allowed` connCaps | ||
666 | | otherwise = True | ||
667 | |||
668 | {----------------------------------------------------------------------- | ||
669 | -- Hanshaking | ||
670 | -----------------------------------------------------------------------} | ||
671 | |||
672 | sendHandshake :: Socket -> Handshake -> IO () | ||
673 | sendHandshake sock hs = sendAll sock (S.encode hs) | ||
674 | |||
675 | recvHandshake :: Socket -> IO Handshake | ||
676 | recvHandshake 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 | -- | ||
694 | initiateHandshake :: Socket -> Handshake -> IO Handshake | ||
695 | initiateHandshake sock hs = do | ||
696 | sendHandshake sock hs | ||
697 | recvHandshake sock | ||
698 | |||
699 | data HandshakePair = HandshakePair | ||
700 | { handshakeSent :: !Handshake | ||
701 | , handshakeRecv :: !Handshake | ||
702 | } deriving (Show, Eq) | ||
703 | |||
704 | validatePair :: HandshakePair -> PeerAddr IP -> IO () | ||
705 | validatePair (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. | ||
716 | establishedStats :: HandshakePair -> ConnectionStats | ||
717 | establishedStats 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 | ||
727 | newtype Connected s a = Connected { runConnected :: (ReaderT (Connection s) IO a) } | ||
728 | deriving (Functor, Applicative, Monad | ||
729 | , MonadIO, MonadReader (Connection s), MonadThrow | ||
730 | ) | ||
731 | |||
732 | instance 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. | ||
738 | type Wire s a = ConduitM Message Message (Connected s) a | ||
739 | |||
740 | {----------------------------------------------------------------------- | ||
741 | -- Wrapper | ||
742 | -----------------------------------------------------------------------} | ||
743 | |||
744 | putStats :: ChannelSide -> Message -> Connected s () | ||
745 | putStats side msg = connStats %= addStats side (stats msg) | ||
746 | |||
747 | validate :: ChannelSide -> Message -> Connected s () | ||
748 | validate 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 | |||
756 | trackFlow :: ChannelSide -> Wire s () | ||
757 | trackFlow side = iterM $ do | ||
758 | validate side | ||
759 | putStats side | ||
760 | |||
761 | {----------------------------------------------------------------------- | ||
762 | -- Setup | ||
763 | -----------------------------------------------------------------------} | ||
764 | |||
765 | -- System.Timeout.timeout multiplier | ||
766 | seconds :: Int | ||
767 | seconds = 1000000 | ||
768 | |||
769 | sinkChan :: MonadIO m => Chan Message -> Sink Message m () | ||
770 | sinkChan chan = await >>= maybe (return ()) (liftIO . writeChan chan) | ||
771 | |||
772 | sourceChan :: MonadIO m => Int -> Chan Message -> Source m Message | ||
773 | sourceChan 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'. | ||
778 | runWire :: Wire s () -> Socket -> Chan Message -> Connection s -> IO () | ||
779 | runWire 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'. | ||
789 | recvMessage :: Wire s Message | ||
790 | recvMessage = await >>= maybe (monadThrow PeerDisconnected) return | ||
791 | |||
792 | -- | You can also use 'yield'. | ||
793 | sendMessage :: PeerMessage msg => msg -> Wire s () | ||
794 | sendMessage msg = do | ||
795 | ecaps <- use connExtCaps | ||
796 | yield $ envelop ecaps msg | ||
797 | |||
798 | getMaxQueueLength :: Connected s Int | ||
799 | getMaxQueueLength = do | ||
800 | advertisedLen <- ehsQueueLength <$> use connRemoteEhs | ||
801 | defaultLen <- asks (requestQueueLength . connOptions) | ||
802 | return $ fromMaybe defaultLen advertisedLen | ||
803 | |||
804 | -- | Filter pending messages from send buffer. | ||
805 | filterQueue :: (Message -> Bool) -> Wire s () | ||
806 | filterQueue 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. | ||
811 | disconnectPeer :: Wire s a | ||
812 | disconnectPeer = monadThrow DisconnectPeer | ||
813 | |||
814 | extendedHandshake :: ExtendedCaps -> Wire s () | ||
815 | extendedHandshake 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 | |||
825 | rehandshake :: ExtendedCaps -> Wire s () | ||
826 | rehandshake caps = error "rehandshake" | ||
827 | |||
828 | reconnect :: Wire s () | ||
829 | reconnect = error "reconnect" | ||
830 | |||
831 | data 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'. | ||
838 | data ConnectionPrefs = ConnectionPrefs | ||
839 | { prefOptions :: !Options | ||
840 | , prefProtocol :: !ProtocolName | ||
841 | , prefCaps :: !Caps | ||
842 | , prefExtCaps :: !ExtendedCaps | ||
843 | } deriving (Show, Eq) | ||
844 | |||
845 | instance Default ConnectionPrefs where | ||
846 | def = ConnectionPrefs | ||
847 | { prefOptions = def | ||
848 | , prefProtocol = def | ||
849 | , prefCaps = def | ||
850 | , prefExtCaps = def | ||
851 | } | ||
852 | |||
853 | normalize :: ConnectionPrefs -> ConnectionPrefs | ||
854 | normalize = error "normalize" | ||
855 | |||
856 | -- | Bridge between 'Connection' and 'Network.BitTorrent.Exchange.Session'. | ||
857 | data SessionLink s = SessionLink | ||
858 | { linkTopic :: !(InfoHash) | ||
859 | , linkPeerId :: !(PeerId) | ||
860 | , linkMetadataSize :: !(Maybe Int) | ||
861 | , linkOutputChan :: !(Maybe (Chan Message)) | ||
862 | , linkSession :: !(s) | ||
863 | } | ||
864 | |||
865 | data ConnectionConfig s = ConnectionConfig | ||
866 | { cfgPrefs :: !(ConnectionPrefs) | ||
867 | , cfgSession :: !(SessionLink s) | ||
868 | , cfgWire :: !(Wire s ()) | ||
869 | } | ||
870 | |||
871 | configHandshake :: ConnectionConfig s -> Handshake | ||
872 | configHandshake 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 | -- | ||
893 | data 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. | ||
901 | pendingHandshake :: PendingConnection -> Handshake | ||
902 | pendingHandshake 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 | -- | ||
914 | newPendingConnection :: Socket -> PeerAddr IP -> IO PendingConnection | ||
915 | newPendingConnection 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'. | ||
928 | closePending :: PendingConnection -> IO () | ||
929 | closePending PendingConnection {..} = do | ||
930 | close pendingSock | ||
931 | |||
932 | {----------------------------------------------------------------------- | ||
933 | -- Connection setup | ||
934 | -----------------------------------------------------------------------} | ||
935 | |||
936 | chanToSock :: Int -> Chan Message -> Socket -> IO () | ||
937 | chanToSock ka chan sock = | ||
938 | sourceChan ka chan $= conduitPut S.put C.$$ sinkSocket sock | ||
939 | |||
940 | afterHandshaking :: ChannelSide -> PeerAddr IP -> Socket -> HandshakePair | ||
941 | -> ConnectionConfig s -> IO () | ||
942 | afterHandshaking 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 | -- | ||
982 | connectWire :: PeerAddr IP -> ConnectionConfig s -> IO () | ||
983 | connectWire 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 | -- | ||
998 | acceptWire :: PendingConnection -> ConnectionConfig s -> IO () | ||
999 | acceptWire 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. | ||
1011 | resizeBitfield :: Int -> Connected s () | ||
1012 | resizeBitfield 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 #-} | ||
15 | module 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 | |||
32 | import Control.Applicative | ||
33 | import Control.Concurrent | ||
34 | import Control.Lens | ||
35 | import Control.Monad.State | ||
36 | import Data.BEncode as BE | ||
37 | import Data.ByteString as BS | ||
38 | import Data.ByteString.Lazy as BL | ||
39 | import Data.Default | ||
40 | import Data.List as L | ||
41 | import Data.Maybe | ||
42 | import Data.Map as M | ||
43 | import Data.Tuple | ||
44 | |||
45 | import Data.Torrent as Torrent | ||
46 | import Network.Address | ||
47 | import Network.BitTorrent.Exchange.Bitfield as BF | ||
48 | import Network.BitTorrent.Exchange.Block as Block | ||
49 | import Network.BitTorrent.Exchange.Message as Msg | ||
50 | import System.Torrent.Storage (Storage, writePiece) | ||
51 | |||
52 | |||
53 | {----------------------------------------------------------------------- | ||
54 | -- Class | ||
55 | -----------------------------------------------------------------------} | ||
56 | |||
57 | type Updates s a = StateT s IO a | ||
58 | |||
59 | runDownloadUpdates :: MVar s -> Updates s a -> IO a | ||
60 | runDownloadUpdates var m = modifyMVar var (fmap swap . runStateT m) | ||
61 | |||
62 | class 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 | |||
96 | data MetadataDownload = MetadataDownload | ||
97 | { _pendingPieces :: [(PeerAddr IP, PieceIx)] | ||
98 | , _bucket :: Bucket | ||
99 | , _topic :: InfoHash | ||
100 | } | ||
101 | |||
102 | makeLenses ''MetadataDownload | ||
103 | |||
104 | -- | Create a new scheduler for infodict of the given size. | ||
105 | metadataDownload :: Int -> InfoHash -> MetadataDownload | ||
106 | metadataDownload ps = MetadataDownload [] (Block.empty ps) | ||
107 | |||
108 | instance Default MetadataDownload where | ||
109 | def = error "instance Default MetadataDownload" | ||
110 | |||
111 | --cancelPending :: PieceIx -> Updates () | ||
112 | cancelPending pix = pendingPieces %= L.filter ((pix ==) . snd) | ||
113 | |||
114 | instance 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 | |||
195 | data PieceEntry = PieceEntry | ||
196 | { pending :: [(PeerAddr IP, BlockIx)] | ||
197 | , stalled :: Bucket | ||
198 | } | ||
199 | |||
200 | pieceEntry :: PieceSize -> PieceEntry | ||
201 | pieceEntry s = PieceEntry [] (Block.empty s) | ||
202 | |||
203 | isEmpty :: PieceEntry -> Bool | ||
204 | isEmpty 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 | |||
211 | data ContentDownload = ContentDownload | ||
212 | { inprogress :: !(Map PieceIx PieceEntry) | ||
213 | , bitfield :: !Bitfield | ||
214 | , pieceSize :: !PieceSize | ||
215 | , contentStorage :: Storage | ||
216 | } | ||
217 | |||
218 | contentDownload :: Bitfield -> PieceSize -> Storage -> ContentDownload | ||
219 | contentDownload = ContentDownload M.empty | ||
220 | |||
221 | --modifyEntry :: PieceIx -> (PieceEntry -> PieceEntry) -> DownloadUpdates () | ||
222 | modifyEntry 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 | |||
230 | instance 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 @@ | |||
1 | module Network.BitTorrent.Exchange.Manager | ||
2 | ( Options (..) | ||
3 | , Manager | ||
4 | , Handler | ||
5 | , newManager | ||
6 | , closeManager | ||
7 | ) where | ||
8 | |||
9 | import Control.Concurrent | ||
10 | import Control.Exception hiding (Handler) | ||
11 | import Control.Monad | ||
12 | import Data.Default | ||
13 | import Network.Socket | ||
14 | |||
15 | import Data.Torrent | ||
16 | import Network.Address | ||
17 | import Network.BitTorrent.Exchange.Connection hiding (Options) | ||
18 | import Network.BitTorrent.Exchange.Session | ||
19 | |||
20 | |||
21 | data Options = Options | ||
22 | { optBacklog :: Int | ||
23 | , optPeerAddr :: PeerAddr IP | ||
24 | } deriving (Show, Eq) | ||
25 | |||
26 | instance Default Options where | ||
27 | def = Options | ||
28 | { optBacklog = maxListenQueue | ||
29 | , optPeerAddr = def | ||
30 | } | ||
31 | |||
32 | data Manager = Manager | ||
33 | { listener :: !ThreadId | ||
34 | } | ||
35 | |||
36 | type Handler = InfoHash -> IO Session | ||
37 | |||
38 | handleNewConn :: Socket -> PeerAddr IP -> Handler -> IO () | ||
39 | handleNewConn sock addr handler = do | ||
40 | conn <- newPendingConnection sock addr | ||
41 | ses <- handler (pendingTopic conn) `onException` closePending conn | ||
42 | establish conn ses | ||
43 | |||
44 | listenIncoming :: Options -> Handler -> IO () | ||
45 | listenIncoming 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 | |||
55 | newManager :: Options -> Handler -> IO Manager | ||
56 | newManager opts handler = do | ||
57 | tid <- forkIO $ listenIncoming opts handler | ||
58 | return (Manager tid) | ||
59 | |||
60 | closeManager :: Manager -> IO () | ||
61 | closeManager 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 #-} | ||
36 | module 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 | |||
91 | import Control.Applicative | ||
92 | import Control.Arrow ((&&&), (***)) | ||
93 | import Control.Monad (when) | ||
94 | import Data.Attoparsec.ByteString.Char8 as BS | ||
95 | import Data.BEncode as BE | ||
96 | import Data.BEncode.BDict as BE | ||
97 | import Data.BEncode.Internal as BE (ppBEncode, parser) | ||
98 | import Data.BEncode.Types (BDict) | ||
99 | import Data.Bits | ||
100 | import Data.ByteString as BS | ||
101 | import Data.ByteString.Char8 as BC | ||
102 | import Data.ByteString.Lazy as BL | ||
103 | import Data.Default | ||
104 | import Data.List as L | ||
105 | import Data.Map.Strict as M | ||
106 | import Data.Maybe | ||
107 | import Data.Monoid | ||
108 | import Data.Ord | ||
109 | import Data.Serialize as S | ||
110 | import Data.String | ||
111 | import Data.Text as T | ||
112 | import Data.Typeable | ||
113 | import Data.Word | ||
114 | import Data.IP | ||
115 | import Network | ||
116 | import Network.Socket hiding (KeepAlive) | ||
117 | import Text.PrettyPrint as PP hiding ((<>)) | ||
118 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
119 | |||
120 | import Data.Torrent hiding (Piece (..)) | ||
121 | import qualified Data.Torrent as P (Piece (..)) | ||
122 | import Network.Address | ||
123 | import Network.BitTorrent.Exchange.Bitfield | ||
124 | import Network.BitTorrent.Exchange.Block | ||
125 | |||
126 | {----------------------------------------------------------------------- | ||
127 | -- Capabilities | ||
128 | -----------------------------------------------------------------------} | ||
129 | |||
130 | -- | | ||
131 | class 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 | |||
143 | ppCaps :: Capabilities caps => Pretty (Ext caps) => caps -> Doc | ||
144 | ppCaps = 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 | -- | ||
154 | data 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. | ||
161 | instance 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. | ||
167 | extMask :: Extension -> Word64 | ||
168 | extMask ExtDHT = 0x01 | ||
169 | extMask ExtFast = 0x04 | ||
170 | extMask ExtExtended = 0x100000 | ||
171 | |||
172 | {----------------------------------------------------------------------- | ||
173 | -- Capabilities | ||
174 | -----------------------------------------------------------------------} | ||
175 | |||
176 | -- | Capabilities is a set of 'Extension's usually sent in 'Handshake' | ||
177 | -- messages. | ||
178 | newtype Caps = Caps Word64 | ||
179 | deriving (Show, Eq) | ||
180 | |||
181 | -- | Render set of extensions as comma separated list. | ||
182 | instance Pretty Caps where | ||
183 | pPrint = ppCaps | ||
184 | {-# INLINE pPrint #-} | ||
185 | |||
186 | -- | The empty set. | ||
187 | instance Default Caps where | ||
188 | def = Caps 0 | ||
189 | {-# INLINE def #-} | ||
190 | |||
191 | -- | Monoid under intersection. 'mempty' includes all known extensions. | ||
192 | instance 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. | ||
200 | instance Serialize Caps where | ||
201 | put (Caps caps) = S.putWord64be caps | ||
202 | {-# INLINE put #-} | ||
203 | |||
204 | get = Caps <$> S.getWord64be | ||
205 | {-# INLINE get #-} | ||
206 | |||
207 | instance 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 | |||
220 | maxProtocolNameSize :: Word8 | ||
221 | maxProtocolNameSize = maxBound | ||
222 | |||
223 | -- | The protocol name is used to identify to the local peer which | ||
224 | -- version of BTP the remote peer uses. | ||
225 | newtype 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. | ||
231 | instance Default ProtocolName where | ||
232 | def = ProtocolName "BitTorrent protocol" | ||
233 | |||
234 | instance Show ProtocolName where | ||
235 | show (ProtocolName bs) = show bs | ||
236 | |||
237 | instance Pretty ProtocolName where | ||
238 | pPrint (ProtocolName bs) = PP.text $ BC.unpack bs | ||
239 | |||
240 | instance 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 | |||
246 | instance 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 | -- | ||
259 | data 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 | |||
280 | instance 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. | ||
289 | instance 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. | ||
297 | handshakeSize :: Word8 -> Int | ||
298 | handshakeSize n = 1 + fromIntegral n + 8 + 20 + 20 | ||
299 | |||
300 | -- | Maximum size of handshake message in bytes. | ||
301 | handshakeMaxSize :: Int | ||
302 | handshakeMaxSize = handshakeSize maxProtocolNameSize | ||
303 | |||
304 | -- | Handshake with default protocol string and reserved bitmask. | ||
305 | defaultHandshake :: InfoHash -> PeerId -> Handshake | ||
306 | defaultHandshake = Handshake def def | ||
307 | |||
308 | handshakeStats :: Handshake -> ByteStats | ||
309 | handshakeStats (Handshake (ProtocolName bs) _ _ _) | ||
310 | = ByteStats 1 (BS.length bs + 8 + 20 + 20) 0 | ||
311 | |||
312 | {----------------------------------------------------------------------- | ||
313 | -- Stats | ||
314 | -----------------------------------------------------------------------} | ||
315 | |||
316 | -- | Number of bytes. | ||
317 | type 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. | ||
322 | data 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 | |||
336 | instance 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. | ||
347 | instance Default ByteStats where | ||
348 | def = ByteStats 0 0 0 | ||
349 | |||
350 | -- | Monoid under addition. | ||
351 | instance 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. | ||
360 | byteLength :: ByteStats -> Int | ||
361 | byteLength ByteStats {..} = overhead + control + payload | ||
362 | |||
363 | {----------------------------------------------------------------------- | ||
364 | -- Regular messages | ||
365 | -----------------------------------------------------------------------} | ||
366 | |||
367 | -- | Messages which can be sent after handshaking. Minimal complete | ||
368 | -- definition: 'envelop'. | ||
369 | class 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'. | ||
400 | data 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 | |||
410 | instance 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 | |||
416 | instance 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. | ||
429 | data 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 | |||
441 | instance Pretty Available where | ||
442 | pPrint (Have ix ) = "Have" <+> int ix | ||
443 | pPrint (Bitfield _ ) = "Bitfield" | ||
444 | |||
445 | instance 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. | ||
460 | data 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 | |||
474 | instance 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 | |||
479 | instance 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. | ||
489 | defaultRequestQueueLength :: Int | ||
490 | defaultRequestQueueLength = 1 | ||
491 | |||
492 | {----------------------------------------------------------------------- | ||
493 | -- Fast messages | ||
494 | -----------------------------------------------------------------------} | ||
495 | |||
496 | -- | BEP6 messages. | ||
497 | data 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 | |||
521 | instance 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 | |||
528 | instance 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 | |||
549 | data ExtendedExtension | ||
550 | = ExtMetadata -- ^ BEP 9: Extension for Peers to Send Metadata Files | ||
551 | deriving (Show, Eq, Ord, Enum, Bounded, Typeable) | ||
552 | |||
553 | instance IsString ExtendedExtension where | ||
554 | fromString = fromMaybe (error msg) . fromKey . fromString | ||
555 | where | ||
556 | msg = "fromString: could not parse ExtendedExtension" | ||
557 | |||
558 | instance Pretty ExtendedExtension where | ||
559 | pPrint ExtMetadata = "Extension for Peers to Send Metadata Files" | ||
560 | |||
561 | fromKey :: BKey -> Maybe ExtendedExtension | ||
562 | fromKey "ut_metadata" = Just ExtMetadata | ||
563 | fromKey _ = Nothing | ||
564 | {-# INLINE fromKey #-} | ||
565 | |||
566 | toKey :: ExtendedExtension -> BKey | ||
567 | toKey ExtMetadata = "ut_metadata" | ||
568 | {-# INLINE toKey #-} | ||
569 | |||
570 | type ExtendedMessageId = Word8 | ||
571 | |||
572 | extId :: ExtendedExtension -> ExtendedMessageId | ||
573 | extId ExtMetadata = 1 | ||
574 | {-# INLINE extId #-} | ||
575 | |||
576 | type 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 | -- | ||
581 | newtype ExtendedCaps = ExtendedCaps { extendedCaps :: ExtendedMap } | ||
582 | deriving (Show, Eq) | ||
583 | |||
584 | instance Pretty ExtendedCaps where | ||
585 | pPrint = ppCaps | ||
586 | {-# INLINE pPrint #-} | ||
587 | |||
588 | -- | The empty set. | ||
589 | instance 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 | -- | ||
599 | instance Monoid ExtendedCaps where | ||
600 | mempty = toCaps [minBound..maxBound] | ||
601 | mappend (ExtendedCaps a) (ExtendedCaps b) = | ||
602 | ExtendedCaps (M.intersection a b) | ||
603 | |||
604 | appendBDict :: BDict -> ExtendedMap -> ExtendedMap | ||
605 | appendBDict (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 | ||
609 | appendBDict Nil caps = caps | ||
610 | |||
611 | -- | Handshake compatible encoding. | ||
612 | instance 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 | |||
619 | instance 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 | |||
630 | remoteMessageId :: ExtendedExtension -> ExtendedCaps -> ExtendedMessageId | ||
631 | remoteMessageId 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 | -- | ||
643 | data 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 | |||
677 | extHandshakeId :: ExtendedMessageId | ||
678 | extHandshakeId = 0 | ||
679 | |||
680 | -- | Default 'Request' queue size. | ||
681 | defaultQueueLength :: Int | ||
682 | defaultQueueLength = 1 | ||
683 | |||
684 | -- | All fields are empty. | ||
685 | instance Default ExtendedHandshake where | ||
686 | def = ExtendedHandshake def def def def def def def def | ||
687 | |||
688 | instance 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 | |||
705 | instance 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 | |||
730 | getYourIp :: Maybe BValue -> BE.Get (Maybe IP) | ||
731 | getYourIp 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 | |||
740 | instance Pretty ExtendedHandshake where | ||
741 | pPrint = PP.text . show | ||
742 | |||
743 | -- | NOTE: Approximated 'stats'. | ||
744 | instance 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'. | ||
755 | nullExtendedHandshake :: ExtendedCaps -> ExtendedHandshake | ||
756 | nullExtendedHandshake 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 | -- | ||
776 | data 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'. | ||
810 | type MetadataId = Int | ||
811 | |||
812 | msg_type_key, piece_key, total_size_key :: BKey | ||
813 | msg_type_key = "msg_type" | ||
814 | piece_key = "piece" | ||
815 | total_size_key = "total_size" | ||
816 | |||
817 | -- | BEP9 compatible encoding. | ||
818 | instance 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. | ||
845 | instance 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'. | ||
852 | instance 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. | ||
867 | metadataPieceSize :: PieceSize | ||
868 | metadataPieceSize = 16 * 1024 | ||
869 | |||
870 | isLastPiece :: P.Piece a -> Int -> Bool | ||
871 | isLastPiece 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. | ||
878 | isValidPiece :: P.Piece BL.ByteString -> Int -> Bool | ||
879 | isValidPiece p @ P.Piece {..} total | ||
880 | | isLastPiece p total = pieceSize p <= metadataPieceSize | ||
881 | | otherwise = pieceSize p == metadataPieceSize | ||
882 | |||
883 | setMetadataPayload :: BS.ByteString -> ExtendedMetadata -> ExtendedMetadata | ||
884 | setMetadataPayload bs (MetadataData (P.Piece pix _) t) = | ||
885 | MetadataData (P.Piece pix bs) t | ||
886 | setMetadataPayload _ msg = msg | ||
887 | |||
888 | getMetadataPayload :: ExtendedMetadata -> Maybe BS.ByteString | ||
889 | getMetadataPayload (MetadataData (P.Piece _ bs) _) = Just bs | ||
890 | getMetadataPayload _ = 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 | -- | ||
899 | maxMetadataBDictSize :: Int | ||
900 | maxMetadataBDictSize = 16 * 1024 | ||
901 | |||
902 | maxMetadataSize :: Int | ||
903 | maxMetadataSize = 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 | ||
909 | getMetadata :: Int -> S.Get ExtendedMetadata | ||
910 | getMetadata 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 | |||
927 | putMetadata :: ExtendedMetadata -> BL.ByteString | ||
928 | putMetadata 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. | ||
937 | defaultMetadataFactor :: Int | ||
938 | defaultMetadataFactor = 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. | ||
945 | defaultMaxInfoDictSize :: Int | ||
946 | defaultMaxInfoDictSize = 10 * 1024 * 1024 | ||
947 | |||
948 | {----------------------------------------------------------------------- | ||
949 | -- Extension protocol messages | ||
950 | -----------------------------------------------------------------------} | ||
951 | |||
952 | -- | For more info see <http://www.bittorrent.org/beps/bep_0010.html> | ||
953 | data ExtendedMessage | ||
954 | = EHandshake ExtendedHandshake | ||
955 | | EMetadata ExtendedMessageId ExtendedMetadata | ||
956 | | EUnknown ExtendedMessageId BS.ByteString | ||
957 | deriving (Show, Eq, Typeable) | ||
958 | |||
959 | instance 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 | |||
964 | instance 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 | |||
979 | type 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 | -- | ||
987 | data 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 | |||
1007 | instance Default Message where | ||
1008 | def = KeepAlive | ||
1009 | {-# INLINE def #-} | ||
1010 | |||
1011 | -- | Payload bytes are omitted. | ||
1012 | instance 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 | |||
1021 | instance 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. | ||
1042 | instance 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. | ||
1051 | defaultKeepAliveTimeout :: Int | ||
1052 | defaultKeepAliveTimeout = 2 * 60 | ||
1053 | |||
1054 | -- | How often /this/ peer should send 'KeepAlive' messages, in | ||
1055 | -- seconds. | ||
1056 | defaultKeepAliveInterval :: Int | ||
1057 | defaultKeepAliveInterval = 60 | ||
1058 | |||
1059 | getInt :: S.Get Int | ||
1060 | getInt = fromIntegral <$> S.getWord32be | ||
1061 | {-# INLINE getInt #-} | ||
1062 | |||
1063 | putInt :: S.Putter Int | ||
1064 | putInt = 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 | -- | ||
1075 | maxMessageSize :: Int | ||
1076 | maxMessageSize = 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 | -- | ||
1084 | maxBitfieldSize :: Int | ||
1085 | maxBitfieldSize = 1024 * 1024 | ||
1086 | |||
1087 | getBitfield :: Int -> S.Get Bitfield | ||
1088 | getBitfield len | ||
1089 | | len > maxBitfieldSize = fail "BITFIELD message size exceeded limit" | ||
1090 | | otherwise = fromBitmap <$> getByteString len | ||
1091 | |||
1092 | maxBlockSize :: Int | ||
1093 | maxBlockSize = 4 * defaultTransferSize | ||
1094 | |||
1095 | getBlock :: Int -> S.Get (Block BL.ByteString) | ||
1096 | getBlock len | ||
1097 | | len > maxBlockSize = fail "BLOCK message size exceeded limit" | ||
1098 | | otherwise = Block <$> getInt <*> getInt | ||
1099 | <*> getLazyByteString (fromIntegral len) | ||
1100 | {-# INLINE getBlock #-} | ||
1101 | |||
1102 | instance 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 | |||
1142 | statusUpdateId :: StatusUpdate -> MessageId | ||
1143 | statusUpdateId (Choking choking) = fromIntegral (0 + fromEnum choking) | ||
1144 | statusUpdateId (Interested choking) = fromIntegral (2 + fromEnum choking) | ||
1145 | |||
1146 | putStatus :: Putter StatusUpdate | ||
1147 | putStatus su = do | ||
1148 | putInt 1 | ||
1149 | putWord8 (statusUpdateId su) | ||
1150 | |||
1151 | putAvailable :: Putter Available | ||
1152 | putAvailable (Have i) = do | ||
1153 | putInt 5 | ||
1154 | putWord8 0x04 | ||
1155 | putInt i | ||
1156 | putAvailable (Bitfield (toBitmap -> bs)) = do | ||
1157 | putInt $ 1 + fromIntegral (BL.length bs) | ||
1158 | putWord8 0x05 | ||
1159 | putLazyByteString bs | ||
1160 | |||
1161 | putBlock :: Putter (Block BL.ByteString) | ||
1162 | putBlock Block {..} = do | ||
1163 | putInt blkPiece | ||
1164 | putInt blkOffset | ||
1165 | putLazyByteString blkData | ||
1166 | |||
1167 | putTransfer :: Putter Transfer | ||
1168 | putTransfer (Request blk) = putInt 13 >> S.putWord8 0x06 >> S.put blk | ||
1169 | putTransfer (Piece blk) = do | ||
1170 | putInt (9 + blockSize blk) | ||
1171 | putWord8 0x07 | ||
1172 | putBlock blk | ||
1173 | putTransfer (Cancel blk) = putInt 13 >> S.putWord8 0x08 >> S.put blk | ||
1174 | |||
1175 | putPort :: Putter PortNumber | ||
1176 | putPort p = do | ||
1177 | putInt 3 | ||
1178 | putWord8 0x09 | ||
1179 | put p | ||
1180 | |||
1181 | putFast :: Putter FastMessage | ||
1182 | putFast HaveAll = putInt 1 >> putWord8 0x0E | ||
1183 | putFast HaveNone = putInt 1 >> putWord8 0x0F | ||
1184 | putFast (SuggestPiece pix) = putInt 5 >> putWord8 0x0D >> putInt pix | ||
1185 | putFast (RejectRequest i ) = putInt 13 >> putWord8 0x10 >> put i | ||
1186 | putFast (AllowedFast i ) = putInt 5 >> putWord8 0x11 >> putInt i | ||
1187 | |||
1188 | maxEHandshakeSize :: Int | ||
1189 | maxEHandshakeSize = 16 * 1024 | ||
1190 | |||
1191 | getExtendedHandshake :: Int -> S.Get ExtendedHandshake | ||
1192 | getExtendedHandshake 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 | |||
1199 | maxEUnknownSize :: Int | ||
1200 | maxEUnknownSize = 64 * 1024 | ||
1201 | |||
1202 | getExtendedUnknown :: Int -> S.Get BS.ByteString | ||
1203 | getExtendedUnknown len | ||
1204 | | len > maxEUnknownSize = fail "unknown extended message size exceeded limit" | ||
1205 | | otherwise = getByteString len | ||
1206 | |||
1207 | getExtendedMessage :: Int -> S.Get ExtendedMessage | ||
1208 | getExtendedMessage 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. | ||
1217 | extendedMessageId :: MessageId | ||
1218 | extendedMessageId = 20 | ||
1219 | |||
1220 | putExt :: ExtendedMessageId -> BL.ByteString -> Put | ||
1221 | putExt 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! | ||
1229 | putExtendedMessage :: Putter ExtendedMessage | ||
1230 | putExtendedMessage (EHandshake hs) = putExt extHandshakeId $ BE.encode hs | ||
1231 | putExtendedMessage (EMetadata mid msg) = putExt mid $ putMetadata msg | ||
1232 | putExtendedMessage (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 #-} | ||
7 | module 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 | |||
29 | import Control.Applicative | ||
30 | import Control.Concurrent | ||
31 | import Control.Concurrent.Chan.Split as CS | ||
32 | import Control.Concurrent.STM | ||
33 | import Control.Exception hiding (Handler) | ||
34 | import Control.Lens | ||
35 | import Control.Monad as M | ||
36 | import Control.Monad.Logger | ||
37 | import Control.Monad.Reader | ||
38 | import Data.ByteString as BS | ||
39 | import Data.ByteString.Lazy as BL | ||
40 | import Data.Conduit as C (Sink, awaitForever, (=$=), ($=)) | ||
41 | import qualified Data.Conduit as C | ||
42 | import Data.Conduit.List as C | ||
43 | import Data.Map as M | ||
44 | import Data.Monoid | ||
45 | import Data.Set as S | ||
46 | import Data.Text as T | ||
47 | import Data.Typeable | ||
48 | import Text.PrettyPrint hiding ((<>)) | ||
49 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
50 | import System.Log.FastLogger (LogStr, ToLogStr (..)) | ||
51 | |||
52 | import Data.BEncode as BE | ||
53 | import Data.Torrent as Torrent | ||
54 | import Network.BitTorrent.Internal.Types | ||
55 | import Network.Address | ||
56 | import Network.BitTorrent.Exchange.Bitfield as BF | ||
57 | import Network.BitTorrent.Exchange.Block as Block | ||
58 | import Network.BitTorrent.Exchange.Connection | ||
59 | import Network.BitTorrent.Exchange.Download as D | ||
60 | import Network.BitTorrent.Exchange.Message as Message | ||
61 | import System.Torrent.Storage | ||
62 | |||
63 | #if !MIN_VERSION_iproute(1,2,12) | ||
64 | deriving instance Ord IP | ||
65 | #endif | ||
66 | |||
67 | {----------------------------------------------------------------------- | ||
68 | -- Exceptions | ||
69 | -----------------------------------------------------------------------} | ||
70 | |||
71 | data ExchangeError | ||
72 | = InvalidRequest BlockIx StorageFailure | ||
73 | | CorruptedPiece PieceIx | ||
74 | deriving (Show, Typeable) | ||
75 | |||
76 | instance Exception ExchangeError | ||
77 | |||
78 | packException :: Exception e => (e -> ExchangeError) -> IO a -> IO a | ||
79 | packException f m = try m >>= either (throwIO . f) return | ||
80 | |||
81 | {----------------------------------------------------------------------- | ||
82 | -- Session state | ||
83 | -----------------------------------------------------------------------} | ||
84 | -- TODO unmap storage on zero connections | ||
85 | |||
86 | data Cached a = Cached | ||
87 | { cachedValue :: !a | ||
88 | , cachedData :: BL.ByteString -- keep lazy | ||
89 | } | ||
90 | |||
91 | cache :: BEncode a => a -> Cached a | ||
92 | cache s = Cached s (BE.encode s) | ||
93 | |||
94 | -- | Logger function. | ||
95 | type LogFun = Loc -> LogSource -> LogLevel -> LogStr -> IO () | ||
96 | |||
97 | --data SessionStatus = Seeder | Leecher | ||
98 | |||
99 | data 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 | |||
111 | newSessionState :: FilePath -> Either InfoHash InfoDict -> IO SessionState | ||
112 | newSessionState rootPath (Left ih ) = do | ||
113 | WaitingMetadata <$> newMVar def <*> newEmptyMVar <*> pure rootPath | ||
114 | newSessionState 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 | |||
121 | closeSessionState :: SessionState -> IO () | ||
122 | closeSessionState WaitingMetadata {..} = return () | ||
123 | closeSessionState HavingMetadata {..} = close contentStorage | ||
124 | |||
125 | haveMetadata :: InfoDict -> SessionState -> IO SessionState | ||
126 | haveMetadata 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 | } | ||
136 | haveMetadata _ s = return s | ||
137 | |||
138 | {----------------------------------------------------------------------- | ||
139 | -- Session | ||
140 | -----------------------------------------------------------------------} | ||
141 | |||
142 | data 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 | |||
170 | instance 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 | |||
181 | newSession :: 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 | ||
186 | newSession 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 | |||
208 | closeSession :: Session -> IO () | ||
209 | closeSession 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 | |||
220 | withSession :: () | ||
221 | withSession = error "withSession" | ||
222 | |||
223 | {----------------------------------------------------------------------- | ||
224 | -- Logging | ||
225 | -----------------------------------------------------------------------} | ||
226 | |||
227 | instance 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 | |||
235 | logMessage :: MonadLogger m => Message -> m () | ||
236 | logMessage msg = logDebugN $ T.pack (render (pPrint msg)) | ||
237 | |||
238 | logEvent :: MonadLogger m => Text -> m () | ||
239 | logEvent = 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. | ||
256 | pendingConnection :: PeerAddr IP -> Session -> STM Bool | ||
257 | pendingConnection 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. | ||
268 | establishedConnection :: Connected Session () | ||
269 | establishedConnection = 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. | ||
280 | finishedConnection :: Connected Session () | ||
281 | finishedConnection = 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. | ||
289 | closedConnection :: PeerAddr IP -> Session -> STM () | ||
290 | closedConnection addr Session {..} = do | ||
291 | modifyTVar connectionsPending $ S.delete addr | ||
292 | modifyTVar connectionsEstablished $ M.delete addr | ||
293 | |||
294 | getConnectionConfig :: Session -> IO (ConnectionConfig Session) | ||
295 | getConnectionConfig 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 | |||
310 | type Finalizer = IO () | ||
311 | type Runner = (ConnectionConfig Session -> IO ()) | ||
312 | |||
313 | runConnection :: Runner -> Finalizer -> PeerAddr IP -> Session -> IO () | ||
314 | runConnection 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. | ||
332 | connect :: PeerAddr IP -> Session -> IO () | ||
333 | connect 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'. | ||
341 | establish :: PendingConnection -> Session -> IO () | ||
342 | establish conn = runConnection (acceptWire conn) (closePending conn) | ||
343 | (pendingPeer conn) | ||
344 | |||
345 | -- | Conduit version of 'connect'. | ||
346 | connectSink :: MonadIO m => Session -> Sink [PeerAddr IPv4] m () | ||
347 | connectSink s = C.mapM_ (liftIO . connectBatch) | ||
348 | where | ||
349 | connectBatch = M.mapM_ (\ addr -> connect (IPv4 <$> addr) s) | ||
350 | |||
351 | -- | Why do we need this message? | ||
352 | type BroadcastMessage = ExtendedCaps -> Message | ||
353 | |||
354 | broadcast :: BroadcastMessage -> Session -> IO () | ||
355 | broadcast = error "broadcast" | ||
356 | |||
357 | {----------------------------------------------------------------------- | ||
358 | -- Helpers | ||
359 | -----------------------------------------------------------------------} | ||
360 | |||
361 | waitMVar :: MVar a -> IO () | ||
362 | waitMVar m = withMVar m (const (return ())) | ||
363 | |||
364 | -- This function appear in new GHC "out of box". (moreover it is atomic) | ||
365 | tryReadMVar :: MVar a -> IO (Maybe a) | ||
366 | tryReadMVar m = do | ||
367 | ma <- tryTakeMVar m | ||
368 | maybe (return ()) (putMVar m) ma | ||
369 | return ma | ||
370 | |||
371 | readBlock :: BlockIx -> Storage -> IO (Block BL.ByteString) | ||
372 | readBlock 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 | -- | | ||
381 | tryReadMetadataBlock :: PieceIx | ||
382 | -> Connected Session (Maybe (Torrent.Piece BS.ByteString, Int)) | ||
383 | tryReadMetadataBlock pix = do | ||
384 | Session {..} <- asks connSession | ||
385 | s <- liftIO (readMVar sessionState) | ||
386 | case s of | ||
387 | WaitingMetadata {..} -> error "tryReadMetadataBlock" | ||
388 | HavingMetadata {..} -> error "tryReadMetadataBlock" | ||
389 | |||
390 | sendBroadcast :: PeerMessage msg => msg -> Wire Session () | ||
391 | sendBroadcast msg = do | ||
392 | Session {..} <- asks connSession | ||
393 | error "sendBroadcast" | ||
394 | -- liftIO $ msg `broadcast` sessionConnections | ||
395 | |||
396 | waitMetadata :: Session -> IO InfoDict | ||
397 | waitMetadata Session {..} = do | ||
398 | s <- readMVar sessionState | ||
399 | case s of | ||
400 | WaitingMetadata {..} -> readMVar metadataCompleted | ||
401 | HavingMetadata {..} -> return (cachedValue metadataCache) | ||
402 | |||
403 | takeMetadata :: Session -> IO (Maybe InfoDict) | ||
404 | takeMetadata 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. | ||
415 | type Trigger = Wire Session () | ||
416 | |||
417 | interesting :: Trigger | ||
418 | interesting = do | ||
419 | addr <- asks connRemoteAddr | ||
420 | sendMessage (Interested True) | ||
421 | sendMessage (Choking False) | ||
422 | tryFillRequestQueue | ||
423 | |||
424 | fillRequestQueue :: Trigger | ||
425 | fillRequestQueue = 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 | |||
435 | tryFillRequestQueue :: Trigger | ||
436 | tryFillRequestQueue = do | ||
437 | allowed <- canDownload <$> use connStatus | ||
438 | when allowed $ do | ||
439 | fillRequestQueue | ||
440 | |||
441 | {----------------------------------------------------------------------- | ||
442 | -- Incoming message handling | ||
443 | -----------------------------------------------------------------------} | ||
444 | |||
445 | type Handler msg = msg -> Wire Session () | ||
446 | |||
447 | handleStatus :: Handler StatusUpdate | ||
448 | handleStatus 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 | |||
458 | handleAvailable :: Handler Available | ||
459 | handleAvailable 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 | |||
474 | handleTransfer :: Handler Transfer | ||
475 | handleTransfer (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 | |||
487 | handleTransfer (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 | |||
502 | handleTransfer (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 | |||
512 | waitForMetadata :: Trigger | ||
513 | waitForMetadata = 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 | |||
522 | tryRequestMetadataBlock :: Trigger | ||
523 | tryRequestMetadataBlock = do | ||
524 | mpix <- lift $ undefined --withMetadataUpdates Metadata.scheduleBlock | ||
525 | case mpix of | ||
526 | Nothing -> error "tryRequestMetadataBlock" | ||
527 | Just pix -> sendMessage (MetadataRequest pix) | ||
528 | |||
529 | handleMetadata :: Handler ExtendedMetadata | ||
530 | handleMetadata (MetadataRequest pix) = | ||
531 | lift (tryReadMetadataBlock pix) >>= sendMessage . mkResponse | ||
532 | where | ||
533 | mkResponse Nothing = MetadataReject pix | ||
534 | mkResponse (Just (piece, total)) = MetadataData piece total | ||
535 | |||
536 | handleMetadata (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 | |||
545 | handleMetadata (MetadataReject pix) = do | ||
546 | lift $ undefined -- withMetadataUpdates (Metadata.cancelPending pix) | ||
547 | |||
548 | handleMetadata (MetadataUnknown _ ) = do | ||
549 | logInfoN "Unknown metadata message" | ||
550 | |||
551 | {----------------------------------------------------------------------- | ||
552 | -- Main entry point | ||
553 | -----------------------------------------------------------------------} | ||
554 | |||
555 | acceptRehandshake :: ExtendedHandshake -> Trigger | ||
556 | acceptRehandshake ehs = error "acceptRehandshake" | ||
557 | |||
558 | handleExtended :: Handler ExtendedMessage | ||
559 | handleExtended (EHandshake ehs) = acceptRehandshake ehs | ||
560 | handleExtended (EMetadata _ msg) = handleMetadata msg | ||
561 | handleExtended (EUnknown _ _ ) = logWarnN "Unknown extension message" | ||
562 | |||
563 | handleMessage :: Handler Message | ||
564 | handleMessage KeepAlive = return () | ||
565 | handleMessage (Status s) = handleStatus s | ||
566 | handleMessage (Available msg) = handleAvailable msg | ||
567 | handleMessage (Transfer msg) = handleTransfer msg | ||
568 | handleMessage (Port n) = error "handleMessage" | ||
569 | handleMessage (Fast _) = error "handleMessage" | ||
570 | handleMessage (Extended msg) = handleExtended msg | ||
571 | |||
572 | exchange :: Wire Session () | ||
573 | exchange = do | ||
574 | waitForMetadata | ||
575 | bf <- undefined --getThisBitfield | ||
576 | sendMessage (Bitfield bf) | ||
577 | awaitForever handleMessage | ||
578 | |||
579 | mainWire :: Wire Session () | ||
580 | mainWire = 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 | -- | ||
10 | module 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 | |||
34 | import Control.Applicative | ||
35 | import Data.Monoid | ||
36 | import Data.Default | ||
37 | import Data.Time | ||
38 | import Data.Time.Clock.POSIX | ||
39 | import System.IO.Unsafe | ||
40 | |||
41 | |||
42 | data 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 | |||
58 | instance Default (Cached a) where | ||
59 | def = mempty | ||
60 | |||
61 | instance Functor Cached where | ||
62 | fmap f (Cached t i m a) = Cached t i m (f a) | ||
63 | |||
64 | posixEpoch :: NominalDiffTime | ||
65 | posixEpoch = 1000000000000000000000000000000000000000000000000000000 | ||
66 | |||
67 | instance 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 | |||
76 | instance Alternative Cached where | ||
77 | empty = mempty | ||
78 | (<|>) = error "cached alternative instance: not implemented" | ||
79 | |||
80 | instance Monad Cached where | ||
81 | return = pure | ||
82 | Cached {..} >>= f = Cached | ||
83 | { lastUpdated = undefined | ||
84 | , updateInterval = undefined | ||
85 | , minUpdateInterval = undefined | ||
86 | , cachedData = undefined | ||
87 | } | ||
88 | |||
89 | instance 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 | |||
101 | normalize :: NominalDiffTime -> NominalDiffTime | ||
102 | -> (NominalDiffTime, NominalDiffTime) | ||
103 | normalize a b | ||
104 | | a < b = (a, b) | ||
105 | | otherwise = (b, a) | ||
106 | {-# INLINE normalize #-} | ||
107 | |||
108 | newCached :: NominalDiffTime -> NominalDiffTime -> a -> IO (Cached a) | ||
109 | newCached 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 | |||
119 | newCached_ :: NominalDiffTime -> a -> IO (Cached a) | ||
120 | newCached_ interval x = newCached interval interval x | ||
121 | {-# INLINE newCached_ #-} | ||
122 | |||
123 | expirationTime :: Cached a -> POSIXTime | ||
124 | expirationTime Cached {..} = undefined | ||
125 | |||
126 | isAlive :: Cached a -> IO Bool | ||
127 | isAlive Cached {..} = do | ||
128 | currentTime <- getPOSIXTime | ||
129 | return $ lastUpdated + updateInterval > currentTime | ||
130 | |||
131 | isExpired :: Cached a -> IO Bool | ||
132 | isExpired Cached {..} = undefined | ||
133 | |||
134 | isStalled :: Cached a -> IO Bool | ||
135 | isStalled Cached {..} = undefined | ||
136 | |||
137 | canUpdate :: Cached a -> IO (Maybe NominalDiffTime) | ||
138 | canUpdate = undefined --isStaled | ||
139 | |||
140 | shouldUpdate :: Cached a -> IO (Maybe NominalDiffTime) | ||
141 | shouldUpdate = undefined -- isExpired | ||
142 | |||
143 | tryTakeData :: Cached a -> IO (Maybe a) | ||
144 | tryTakeData c = do | ||
145 | alive <- isAlive c | ||
146 | return $ if alive then Just (cachedData c) else Nothing | ||
147 | |||
148 | unsafeTryTakeData :: Cached a -> Maybe a | ||
149 | unsafeTryTakeData = unsafePerformIO . tryTakeData | ||
150 | |||
151 | invalidateData :: Cached a -> IO a -> IO (Cached a) | ||
152 | invalidateData 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 | |||
162 | takeData :: Cached a -> IO a -> IO a | ||
163 | takeData 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 #-} | ||
16 | module 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 | |||
37 | import Control.Applicative | ||
38 | import Control.Lens hiding ((%=)) | ||
39 | import Data.ByteString.Lazy.Builder as BS | ||
40 | import Data.ByteString.Lazy.Builder.ASCII as BS | ||
41 | import Data.Default | ||
42 | import Data.Monoid | ||
43 | import Data.Serialize as S | ||
44 | import Data.Ratio | ||
45 | import Data.Word | ||
46 | import Network.HTTP.Types.QueryLike | ||
47 | import Text.PrettyPrint as PP | ||
48 | import 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 | -- | ||
56 | data 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. | ||
65 | instance 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 | |||
76 | instance Default Progress where | ||
77 | def = Progress 0 0 0 | ||
78 | {-# INLINE def #-} | ||
79 | |||
80 | -- | Can be used to aggregate total progress. | ||
81 | instance 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 | |||
92 | instance QueryValueLike Builder where | ||
93 | toQueryValue = toQueryValue . BS.toLazyByteString | ||
94 | |||
95 | instance QueryValueLike Word64 where | ||
96 | toQueryValue = toQueryValue . BS.word64Dec | ||
97 | |||
98 | -- | HTTP Tracker protocol compatible encoding. | ||
99 | instance QueryLike Progress where | ||
100 | toQuery Progress {..} = | ||
101 | [ ("uploaded" , toQueryValue _uploaded) | ||
102 | , ("left" , toQueryValue _left) | ||
103 | , ("downloaded", toQueryValue _downloaded) | ||
104 | ] | ||
105 | |||
106 | instance 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 | -- | ||
118 | startProgress :: Integer -> Progress | ||
119 | startProgress = Progress 0 0 . fromIntegral | ||
120 | {-# INLINE startProgress #-} | ||
121 | |||
122 | -- | Used when the client download some data from /any/ peer. | ||
123 | downloadedProgress :: Int -> Progress -> Progress | ||
124 | downloadedProgress (fromIntegral -> amount) | ||
125 | = (left -~ amount) | ||
126 | . (downloaded +~ amount) | ||
127 | {-# INLINE downloadedProgress #-} | ||
128 | |||
129 | -- | Used when the client upload some data to /any/ peer. | ||
130 | uploadedProgress :: Int -> Progress -> Progress | ||
131 | uploadedProgress (fromIntegral -> amount) = uploaded +~ amount | ||
132 | {-# INLINE uploadedProgress #-} | ||
133 | |||
134 | -- | Used when leecher join client session. | ||
135 | enqueuedProgress :: Integer -> Progress -> Progress | ||
136 | enqueuedProgress amount = left +~ fromIntegral amount | ||
137 | {-# INLINE enqueuedProgress #-} | ||
138 | |||
139 | -- | Used when leecher leave client session. | ||
140 | -- (e.g. user deletes not completed torrent) | ||
141 | dequeuedProgress :: Integer -> Progress -> Progress | ||
142 | dequeuedProgress amount = left -~ fromIntegral amount | ||
143 | {-# INLINE dequeuedProgress #-} | ||
144 | |||
145 | ri2rw64 :: Ratio Int -> Ratio Word64 | ||
146 | ri2rw64 x = fromIntegral (numerator x) % fromIntegral (denominator x) | ||
147 | |||
148 | -- | Check global /download/ limit by uploaded \/ downloaded ratio. | ||
149 | canDownload :: Ratio Int -> Progress -> Bool | ||
150 | canDownload limit Progress {..} = _uploaded % _downloaded > ri2rw64 limit | ||
151 | |||
152 | -- | Check global /upload/ limit by downloaded \/ uploaded ratio. | ||
153 | canUpload :: Ratio Int -> Progress -> Bool | ||
154 | canUpload 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 #-} | ||
2 | module Network.BitTorrent.Internal.Types | ||
3 | ( EventSource (..) | ||
4 | ) where | ||
5 | |||
6 | import Control.Concurrent.Chan.Split | ||
7 | |||
8 | class 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 @@ | |||
1 | Layout | ||
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 #-} | ||
13 | module 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 | |||
46 | import Network.BitTorrent.Internal.Cache (tryTakeData, unsafeTryTakeData) | ||
47 | import Network.BitTorrent.Tracker.Message | ||
48 | import Network.BitTorrent.Tracker.List | ||
49 | import Network.BitTorrent.Tracker.RPC | ||
50 | import 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 #-} | ||
13 | module 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 | |||
29 | import Prelude hiding (mapM, foldr) | ||
30 | import Control.Arrow | ||
31 | import Control.Applicative | ||
32 | import Control.Exception | ||
33 | import Data.Default | ||
34 | import Data.List as L (map, elem, any, filter, null) | ||
35 | import Data.Maybe | ||
36 | import Data.Foldable | ||
37 | import Data.Traversable | ||
38 | import Network.URI | ||
39 | import System.Random.Shuffle | ||
40 | |||
41 | import Data.Torrent | ||
42 | import Network.BitTorrent.Tracker.RPC as RPC | ||
43 | |||
44 | {----------------------------------------------------------------------- | ||
45 | -- Tracker list datatype | ||
46 | -----------------------------------------------------------------------} | ||
47 | |||
48 | type TierEntry a = (URI, a) | ||
49 | type 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. | ||
54 | data 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. | ||
60 | instance Default (TrackerList a) where | ||
61 | def = TierList [] | ||
62 | |||
63 | instance 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 | |||
67 | instance 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 | |||
73 | instance 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 | |||
78 | traverseWithURI :: Applicative f | ||
79 | => (TierEntry a -> f b) -> TrackerList a -> f (TrackerList b) | ||
80 | traverseWithURI f (Announce (uri, a)) = (Announce . (,) uri) <$> f (uri, a) | ||
81 | traverseWithURI 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 | -- | ||
108 | addBackup :: [[URI]] -> URI -> [[URI]] | ||
109 | addBackup tiers bkp | ||
110 | | L.any (L.elem bkp) tiers = tiers | ||
111 | | otherwise = tiers ++ [[bkp]] | ||
112 | |||
113 | fixList :: Maybe [[URI]] -> Maybe URI -> Maybe [[URI]] | ||
114 | fixList 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. | ||
121 | trackerList :: Torrent -> TrackerList () | ||
122 | trackerList 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. | ||
132 | shuffleTiers :: TrackerList a -> IO (TrackerList a) | ||
133 | shuffleTiers (Announce a ) = return (Announce a) | ||
134 | shuffleTiers (TierList xs) = TierList <$> mapM shuffleM xs | ||
135 | |||
136 | mapWithURI :: (URI -> a -> b) -> TrackerList a -> TrackerList b | ||
137 | mapWithURI f (Announce (uri, a)) = Announce (uri, f uri a) | ||
138 | mapWithURI f (TierList xs ) = TierList (L.map (L.map mapEntry) xs) | ||
139 | where | ||
140 | mapEntry (uri, a) = (uri, f uri a) | ||
141 | |||
142 | toList :: TrackerList a -> [[TierEntry a]] | ||
143 | toList (Announce e) = [[e]] | ||
144 | toList (TierList xxs) = xxs | ||
145 | |||
146 | {----------------------------------------------------------------------- | ||
147 | -- Special traversals (suppressed RPC exceptions) | ||
148 | -----------------------------------------------------------------------} | ||
149 | |||
150 | catchRPC :: IO a -> IO a -> IO a | ||
151 | catchRPC a b = catch a (f b) | ||
152 | where | ||
153 | f :: a -> RpcException -> a | ||
154 | f = const | ||
155 | |||
156 | throwRPC :: String -> IO a | ||
157 | throwRPC = throwIO . GenericException | ||
158 | |||
159 | -- | Like 'traverse' but ignores 'RpcExceptions'. | ||
160 | traverseAll :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a) | ||
161 | traverseAll 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. | ||
169 | traverseTiers :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a) | ||
170 | traverseTiers 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 #-} | ||
31 | module 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 | |||
99 | import Control.Applicative | ||
100 | import Control.Monad | ||
101 | import Data.BEncode as BE hiding (Result) | ||
102 | import Data.BEncode.BDict as BE | ||
103 | import Data.ByteString as BS | ||
104 | import Data.ByteString.Char8 as BC | ||
105 | import Data.Char as Char | ||
106 | import Data.Convertible | ||
107 | import Data.Default | ||
108 | import Data.Either | ||
109 | import Data.List as L | ||
110 | import Data.Maybe | ||
111 | import Data.Monoid | ||
112 | import Data.Serialize as S hiding (Result) | ||
113 | import Data.String | ||
114 | import Data.Text (Text) | ||
115 | import Data.Text.Encoding | ||
116 | import Data.Typeable | ||
117 | import Data.Word | ||
118 | import Data.IP | ||
119 | import Network | ||
120 | import Network.HTTP.Types.QueryLike | ||
121 | import Network.HTTP.Types.URI hiding (urlEncode) | ||
122 | import Network.HTTP.Types.Status | ||
123 | import Network.Socket hiding (Connected) | ||
124 | import Numeric | ||
125 | import System.Entropy | ||
126 | import Text.Read (readMaybe) | ||
127 | |||
128 | import Data.Torrent | ||
129 | import Network.Address | ||
130 | import Network.BitTorrent.Internal.Progress | ||
131 | |||
132 | {----------------------------------------------------------------------- | ||
133 | -- Events | ||
134 | -----------------------------------------------------------------------} | ||
135 | |||
136 | -- | Events are used to specify which kind of announce query is performed. | ||
137 | data 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. | ||
152 | instance 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 | |||
157 | type EventId = Word32 | ||
158 | |||
159 | -- | UDP tracker encoding event codes. | ||
160 | eventId :: AnnounceEvent -> EventId | ||
161 | eventId Completed = 1 | ||
162 | eventId Started = 2 | ||
163 | eventId Stopped = 3 | ||
164 | |||
165 | -- TODO add Regular event | ||
166 | putEvent :: Putter (Maybe AnnounceEvent) | ||
167 | putEvent Nothing = putWord32be 0 | ||
168 | putEvent (Just e) = putWord32be (eventId e) | ||
169 | |||
170 | getEvent :: S.Get (Maybe AnnounceEvent) | ||
171 | getEvent = 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 | -- | ||
191 | data 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. | ||
224 | instance 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 | |||
260 | instance QueryValueLike PortNumber where | ||
261 | toQueryValue = toQueryValue . show . fromEnum | ||
262 | |||
263 | instance QueryValueLike Word32 where | ||
264 | toQueryValue = toQueryValue . show | ||
265 | |||
266 | instance QueryValueLike Int where | ||
267 | toQueryValue = toQueryValue . show | ||
268 | |||
269 | -- | HTTP tracker protocol compatible encoding. | ||
270 | instance 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. | ||
282 | queryToSimpleQuery :: Query -> SimpleQuery | ||
283 | queryToSimpleQuery = 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. | ||
289 | renderAnnounceQuery :: AnnounceQuery -> SimpleQuery | ||
290 | renderAnnounceQuery = queryToSimpleQuery . toQuery | ||
291 | |||
292 | data 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 | |||
308 | paramName :: QueryParam -> BS.ByteString | ||
309 | paramName ParamInfoHash = "info_hash" | ||
310 | paramName ParamPeerId = "peer_id" | ||
311 | paramName ParamPort = "port" | ||
312 | paramName ParamUploaded = "uploaded" | ||
313 | paramName ParamLeft = "left" | ||
314 | paramName ParamDownloaded = "downloaded" | ||
315 | paramName ParamIP = "ip" | ||
316 | paramName ParamNumWant = "numwant" | ||
317 | paramName ParamEvent = "event" | ||
318 | paramName ParamCompact = "compact" | ||
319 | paramName ParamNoPeerId = "no_peer_id" | ||
320 | {-# INLINE paramName #-} | ||
321 | |||
322 | class FromParam a where | ||
323 | fromParam :: BS.ByteString -> Maybe a | ||
324 | |||
325 | instance FromParam Bool where | ||
326 | fromParam "0" = Just False | ||
327 | fromParam "1" = Just True | ||
328 | fromParam _ = Nothing | ||
329 | |||
330 | instance FromParam InfoHash where | ||
331 | fromParam = either (const Nothing) pure . safeConvert | ||
332 | |||
333 | instance FromParam PeerId where | ||
334 | fromParam = either (const Nothing) pure . safeConvert | ||
335 | |||
336 | instance FromParam Word32 where | ||
337 | fromParam = readMaybe . BC.unpack | ||
338 | |||
339 | instance FromParam Word64 where | ||
340 | fromParam = readMaybe . BC.unpack | ||
341 | |||
342 | instance FromParam Int where | ||
343 | fromParam = readMaybe . BC.unpack | ||
344 | |||
345 | instance FromParam PortNumber where | ||
346 | fromParam bs = fromIntegral <$> (fromParam bs :: Maybe Word32) | ||
347 | |||
348 | instance 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 | -- | ||
358 | data 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 | |||
363 | type ParseResult = Either ParamParseFailure | ||
364 | |||
365 | withError :: ParamParseFailure -> Maybe a -> ParseResult a | ||
366 | withError e = maybe (Left e) Right | ||
367 | |||
368 | reqParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult a | ||
369 | reqParam param xs = do | ||
370 | val <- withError (Missing param) $ L.lookup (paramName param) xs | ||
371 | withError (Invalid param val) (fromParam val) | ||
372 | |||
373 | optParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult (Maybe a) | ||
374 | optParam param ps | ||
375 | | Just x <- L.lookup (paramName param) ps | ||
376 | = pure <$> withError (Invalid param x) (fromParam x) | ||
377 | | otherwise = pure Nothing | ||
378 | |||
379 | parseProgress :: SimpleQuery -> ParseResult Progress | ||
380 | parseProgress params = Progress | ||
381 | <$> reqParam ParamDownloaded params | ||
382 | <*> reqParam ParamLeft params | ||
383 | <*> reqParam ParamUploaded params | ||
384 | |||
385 | -- | Parse announce request from a query string. | ||
386 | parseAnnounceQuery :: SimpleQuery -> ParseResult AnnounceQuery | ||
387 | parseAnnounceQuery 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 | -- | ||
406 | data PeerList ip | ||
407 | = PeerList [PeerAddr IP] | ||
408 | | CompactPeerList [PeerAddr ip] | ||
409 | deriving (Show, Eq, Typeable, Functor) | ||
410 | |||
411 | -- | The empty non-compact peer list. | ||
412 | instance Default (PeerList IP) where | ||
413 | def = PeerList [] | ||
414 | {-# INLINE def #-} | ||
415 | |||
416 | getPeerList :: PeerList IP -> [PeerAddr IP] | ||
417 | getPeerList (PeerList xs) = xs | ||
418 | getPeerList (CompactPeerList xs) = xs | ||
419 | |||
420 | instance 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 | -- | ||
432 | data 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. | ||
458 | instance 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. | ||
469 | instance 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. | ||
530 | instance 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. | ||
554 | instance 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 | -- | ||
566 | defaultNumWant :: Int | ||
567 | defaultNumWant = 50 | ||
568 | |||
569 | -- | Reasonable upper bound of numwant parameter. | ||
570 | defaultMaxNumWant :: Int | ||
571 | defaultMaxNumWant = 200 | ||
572 | |||
573 | -- | Widely used reannounce interval. Note: tracker clients should not | ||
574 | -- use this value! | ||
575 | defaultReannounceInterval :: Int | ||
576 | defaultReannounceInterval = 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. | ||
585 | type 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 | |||
600 | scrapeParam :: BS.ByteString | ||
601 | scrapeParam = "info_hash" | ||
602 | |||
603 | isScrapeParam :: BS.ByteString -> Bool | ||
604 | isScrapeParam = (==) scrapeParam | ||
605 | |||
606 | -- | Parse scrape query to query string. | ||
607 | parseScrapeQuery :: SimpleQuery -> ScrapeQuery | ||
608 | parseScrapeQuery | ||
609 | = catMaybes . L.map (fromParam . snd) . L.filter (isScrapeParam . fst) | ||
610 | |||
611 | -- | Render scrape query to query string. | ||
612 | renderScrapeQuery :: ScrapeQuery -> SimpleQuery | ||
613 | renderScrapeQuery = queryToSimpleQuery . L.map mkPair | ||
614 | where | ||
615 | mkPair ih = (scrapeParam, toQueryValue ih) | ||
616 | |||
617 | -- | Overall information about particular torrent. | ||
618 | data 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. | ||
634 | instance 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. | ||
649 | instance 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. | ||
662 | type 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. | ||
671 | data 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 | |||
696 | instance Default AnnouncePrefs where | ||
697 | def = AnnouncePrefs Nothing Nothing | ||
698 | |||
699 | instance 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. | ||
709 | parseAnnouncePrefs :: SimpleQuery -> AnnouncePrefs | ||
710 | parseAnnouncePrefs params = either (const def) id $ | ||
711 | AnnouncePrefs | ||
712 | <$> optParam ParamCompact params | ||
713 | <*> optParam ParamNoPeerId params | ||
714 | |||
715 | -- | Render announce preferences to query string. | ||
716 | renderAnnouncePrefs :: AnnouncePrefs -> SimpleQuery | ||
717 | renderAnnouncePrefs = queryToSimpleQuery . toQuery | ||
718 | |||
719 | -- | HTTP tracker request with preferences. | ||
720 | data AnnounceRequest = AnnounceRequest | ||
721 | { announceQuery :: AnnounceQuery -- ^ Request query params. | ||
722 | , announcePrefs :: AnnouncePrefs -- ^ Optional advises to the tracker. | ||
723 | } deriving (Show, Eq, Typeable) | ||
724 | |||
725 | instance QueryLike AnnounceRequest where | ||
726 | toQuery AnnounceRequest{..} = | ||
727 | toQuery announcePrefs <> | ||
728 | toQuery announceQuery | ||
729 | |||
730 | -- | Parse announce request from query string. | ||
731 | parseAnnounceRequest :: SimpleQuery -> ParseResult AnnounceRequest | ||
732 | parseAnnounceRequest params = AnnounceRequest | ||
733 | <$> parseAnnounceQuery params | ||
734 | <*> pure (parseAnnouncePrefs params) | ||
735 | |||
736 | -- | Render announce request to query string. | ||
737 | renderAnnounceRequest :: AnnounceRequest -> SimpleQuery | ||
738 | renderAnnounceRequest = queryToSimpleQuery . toQuery | ||
739 | |||
740 | type PathPiece = BS.ByteString | ||
741 | |||
742 | defaultAnnouncePath :: PathPiece | ||
743 | defaultAnnouncePath = "announce" | ||
744 | |||
745 | defaultScrapePath :: PathPiece | ||
746 | defaultScrapePath = "scrape" | ||
747 | |||
748 | missingOffset :: Int | ||
749 | missingOffset = 101 | ||
750 | |||
751 | invalidOffset :: Int | ||
752 | invalidOffset = 150 | ||
753 | |||
754 | parseFailureCode :: ParamParseFailure -> Int | ||
755 | parseFailureCode (Missing param ) = missingOffset + fromEnum param | ||
756 | parseFailureCode (Invalid param _) = invalidOffset + fromEnum param | ||
757 | |||
758 | parseFailureMessage :: ParamParseFailure -> BS.ByteString | ||
759 | parseFailureMessage 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. | ||
764 | announceType :: ByteString | ||
765 | announceType = "text/plain" | ||
766 | |||
767 | -- | HTTP response /content type/ for scrape info. | ||
768 | scrapeType :: ByteString | ||
769 | scrapeType = "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 | -- | ||
776 | parseFailureStatus :: ParamParseFailure -> Status | ||
777 | parseFailureStatus = mkStatus <$> parseFailureCode <*> parseFailureMessage | ||
778 | |||
779 | {----------------------------------------------------------------------- | ||
780 | -- UDP specific message types | ||
781 | -----------------------------------------------------------------------} | ||
782 | |||
783 | genToken :: IO Word64 | ||
784 | genToken = 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. | ||
791 | newtype ConnectionId = ConnectionId Word64 | ||
792 | deriving (Eq, Serialize) | ||
793 | |||
794 | instance Show ConnectionId where | ||
795 | showsPrec _ (ConnectionId cid) = showString "0x" <> showHex cid | ||
796 | |||
797 | initialConnectionId :: ConnectionId | ||
798 | initialConnectionId = ConnectionId 0x41727101980 | ||
799 | |||
800 | -- | Transaction Id is used within a UDP RPC. | ||
801 | newtype TransactionId = TransactionId Word32 | ||
802 | deriving (Eq, Ord, Enum, Bounded, Serialize) | ||
803 | |||
804 | instance Show TransactionId where | ||
805 | showsPrec _ (TransactionId tid) = showString "0x" <> showHex tid | ||
806 | |||
807 | genTransactionId :: IO TransactionId | ||
808 | genTransactionId = (TransactionId . fromIntegral) <$> genToken | ||
809 | |||
810 | data Request | ||
811 | = Connect | ||
812 | | Announce AnnounceQuery | ||
813 | | Scrape ScrapeQuery | ||
814 | deriving Show | ||
815 | |||
816 | data Response | ||
817 | = Connected ConnectionId | ||
818 | | Announced AnnounceInfo | ||
819 | | Scraped [ScrapeEntry] | ||
820 | | Failed Text | ||
821 | deriving Show | ||
822 | |||
823 | responseName :: Response -> String | ||
824 | responseName (Connected _) = "connected" | ||
825 | responseName (Announced _) = "announced" | ||
826 | responseName (Scraped _) = "scraped" | ||
827 | responseName (Failed _) = "failed" | ||
828 | |||
829 | data family Transaction a | ||
830 | data instance Transaction Request = TransactionQ | ||
831 | { connIdQ :: {-# UNPACK #-} !ConnectionId | ||
832 | , transIdQ :: {-# UNPACK #-} !TransactionId | ||
833 | , request :: !Request | ||
834 | } deriving Show | ||
835 | data instance Transaction Response = TransactionR | ||
836 | { transIdR :: {-# UNPACK #-} !TransactionId | ||
837 | , response :: !Response | ||
838 | } deriving Show | ||
839 | |||
840 | -- TODO newtype | ||
841 | newtype MessageId = MessageId Word32 | ||
842 | deriving (Show, Eq, Num, Serialize) | ||
843 | |||
844 | connectId, announceId, scrapeId, errorId :: MessageId | ||
845 | connectId = 0 | ||
846 | announceId = 1 | ||
847 | scrapeId = 2 | ||
848 | errorId = 3 | ||
849 | |||
850 | instance 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 | |||
884 | instance 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 #-} | ||
15 | module 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 | |||
32 | import Control.Exception | ||
33 | import Data.Default | ||
34 | import Data.Typeable | ||
35 | import Network | ||
36 | import Network.URI | ||
37 | import Network.Socket (HostAddress) | ||
38 | |||
39 | import Data.Torrent | ||
40 | import Network.Address | ||
41 | import Network.BitTorrent.Internal.Progress | ||
42 | import Network.BitTorrent.Tracker.Message | ||
43 | import qualified Network.BitTorrent.Tracker.RPC.HTTP as HTTP | ||
44 | import qualified Network.BitTorrent.Tracker.RPC.UDP as UDP | ||
45 | |||
46 | |||
47 | {----------------------------------------------------------------------- | ||
48 | -- Simplified announce | ||
49 | -----------------------------------------------------------------------} | ||
50 | |||
51 | -- | Info to advertise to trackers. | ||
52 | data PeerInfo = PeerInfo | ||
53 | { peerId :: !PeerId | ||
54 | , peerIP :: !(Maybe HostAddress) | ||
55 | , peerPort :: !PortNumber | ||
56 | } deriving (Show, Eq) | ||
57 | |||
58 | instance Default PeerInfo where | ||
59 | def = PeerInfo def Nothing 6881 | ||
60 | |||
61 | -- | Simplified announce query. | ||
62 | data SAnnounceQuery = SAnnounceQuery | ||
63 | { sInfoHash :: InfoHash | ||
64 | , sProgress :: Progress | ||
65 | , sNumWant :: Maybe Int | ||
66 | , sEvent :: Maybe AnnounceEvent | ||
67 | } | ||
68 | |||
69 | fillAnnounceQuery :: PeerInfo -> SAnnounceQuery -> AnnounceQuery | ||
70 | fillAnnounceQuery 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. | ||
85 | data 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 | |||
96 | instance Default Options where | ||
97 | def = Options | ||
98 | { optHttpRPC = def | ||
99 | , optUdpRPC = def | ||
100 | , optMultitracker = True | ||
101 | } | ||
102 | |||
103 | -- | Tracker RPC Manager. | ||
104 | data 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 | -- | ||
117 | newManager :: Options -> PeerInfo -> IO Manager | ||
118 | newManager 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. | ||
126 | closeManager :: Manager -> IO () | ||
127 | closeManager Manager {..} = do | ||
128 | UDP.closeManager udpMgr `finally` HTTP.closeManager httpMgr | ||
129 | |||
130 | -- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'. | ||
131 | withManager :: Options -> PeerInfo -> (Manager -> IO a) -> IO a | ||
132 | withManager opts info = bracket (newManager opts info) closeManager | ||
133 | |||
134 | {----------------------------------------------------------------------- | ||
135 | -- Exceptions | ||
136 | -----------------------------------------------------------------------} | ||
137 | -- TODO Catch IO exceptions on rpc calls (?) | ||
138 | |||
139 | data 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 | |||
146 | instance Exception RpcException | ||
147 | |||
148 | packException :: Exception e => (e -> RpcException) -> IO a -> IO a | ||
149 | packException f m = try m >>= either (throwIO . f) return | ||
150 | {-# INLINE packException #-} | ||
151 | |||
152 | {----------------------------------------------------------------------- | ||
153 | -- RPC calls | ||
154 | -----------------------------------------------------------------------} | ||
155 | |||
156 | dispatch :: URI -> IO a -> IO a -> IO a | ||
157 | dispatch URI {..} http udp | ||
158 | | uriScheme == "http:" || | ||
159 | uriScheme == "https:" = packException HttpException http | ||
160 | | uriScheme == "udp:" = packException UdpException udp | ||
161 | | otherwise = throwIO $ UnrecognizedScheme uriScheme | ||
162 | |||
163 | announce :: Manager -> URI -> SAnnounceQuery -> IO AnnounceInfo | ||
164 | announce 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 | |||
171 | scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo | ||
172 | scrape 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 #-} | ||
14 | module 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 | |||
29 | import Control.Applicative | ||
30 | import Control.Exception | ||
31 | import Control.Monad | ||
32 | import Control.Monad.Trans.Resource | ||
33 | import Data.BEncode as BE | ||
34 | import Data.ByteString as BS | ||
35 | import Data.ByteString.Char8 as BC | ||
36 | import Data.ByteString.Lazy as BL | ||
37 | import Data.Default | ||
38 | import Data.List as L | ||
39 | import Data.Monoid | ||
40 | import Data.Typeable hiding (Proxy) | ||
41 | import Network.URI | ||
42 | import Network.HTTP.Conduit hiding | ||
43 | (Manager, newManager, closeManager, withManager) | ||
44 | import Network.HTTP.Client (defaultManagerSettings) | ||
45 | import Network.HTTP.Client.Internal (setUri) | ||
46 | import qualified Network.HTTP.Conduit as HTTP | ||
47 | import Network.HTTP.Types.Header (hUserAgent) | ||
48 | import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery) | ||
49 | |||
50 | import Data.Torrent (InfoHash) | ||
51 | import Network.Address (libUserAgent) | ||
52 | import Network.BitTorrent.Tracker.Message hiding (Request, Response) | ||
53 | |||
54 | {----------------------------------------------------------------------- | ||
55 | -- Exceptions | ||
56 | -----------------------------------------------------------------------} | ||
57 | |||
58 | data 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 | |||
65 | instance Exception RpcException | ||
66 | |||
67 | packHttpException :: IO a -> IO a | ||
68 | packHttpException m = try m >>= either (throwIO . RequestFailed) return | ||
69 | |||
70 | {----------------------------------------------------------------------- | ||
71 | -- Manager | ||
72 | -----------------------------------------------------------------------} | ||
73 | |||
74 | -- | HTTP tracker specific RPC options. | ||
75 | data 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 | |||
89 | instance 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. | ||
98 | data Manager = Manager | ||
99 | { options :: !Options | ||
100 | , httpMgr :: !HTTP.Manager | ||
101 | } | ||
102 | |||
103 | -- | | ||
104 | newManager :: Options -> IO Manager | ||
105 | newManager opts = Manager opts <$> HTTP.newManager (optHttpOptions opts) | ||
106 | |||
107 | -- | | ||
108 | closeManager :: Manager -> IO () | ||
109 | closeManager Manager {..} = HTTP.closeManager httpMgr | ||
110 | |||
111 | -- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'. | ||
112 | withManager :: Options -> (Manager -> IO a) -> IO a | ||
113 | withManager opts = bracket (newManager opts) closeManager | ||
114 | |||
115 | {----------------------------------------------------------------------- | ||
116 | -- Queries | ||
117 | -----------------------------------------------------------------------} | ||
118 | |||
119 | fillRequest :: Options -> SimpleQuery -> Request -> Request | ||
120 | fillRequest 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 | |||
130 | httpTracker :: BEncode a => Manager -> URI -> SimpleQuery -> IO a | ||
131 | httpTracker 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 | -- | ||
147 | announce :: Manager -> URI -> AnnounceQuery -> IO AnnounceInfo | ||
148 | announce 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 | -- | ||
158 | scrapeURL :: URI -> Maybe URI | ||
159 | scrapeURL 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 | -- | ||
176 | scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo | ||
177 | scrape 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 | -- | ||
186 | scrapeOne :: Manager -> URI -> InfoHash -> IO ScrapeEntry | ||
187 | scrapeOne 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 #-} | ||
18 | module 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 | |||
32 | import Control.Applicative | ||
33 | import Control.Concurrent | ||
34 | import Control.Exception | ||
35 | import Control.Monad | ||
36 | import Data.Default | ||
37 | import Data.IORef | ||
38 | import Data.List as L | ||
39 | import Data.Map as M | ||
40 | import Data.Maybe | ||
41 | import Data.Serialize | ||
42 | import Data.Text as T | ||
43 | import Data.Time | ||
44 | import Data.Time.Clock.POSIX | ||
45 | import Data.Traversable | ||
46 | import Data.Typeable | ||
47 | import Text.Read (readMaybe) | ||
48 | import Network.Socket hiding (Connected, connect, listen) | ||
49 | import Network.Socket.ByteString as BS | ||
50 | import Network.URI | ||
51 | import System.Timeout | ||
52 | |||
53 | import Network.BitTorrent.Tracker.Message | ||
54 | |||
55 | {----------------------------------------------------------------------- | ||
56 | -- Options | ||
57 | -----------------------------------------------------------------------} | ||
58 | |||
59 | -- | 'System.Timeout.timeout' specific. | ||
60 | sec :: Int | ||
61 | sec = 1000000 | ||
62 | |||
63 | -- | See <http://www.bittorrent.org/beps/bep_0015.html#time-outs> | ||
64 | defMinTimeout :: Int | ||
65 | defMinTimeout = 15 | ||
66 | |||
67 | -- | See <http://www.bittorrent.org/beps/bep_0015.html#time-outs> | ||
68 | defMaxTimeout :: Int | ||
69 | defMaxTimeout = 15 * 2 ^ (8 :: Int) | ||
70 | |||
71 | -- | See: <http://www.bittorrent.org/beps/bep_0015.html#time-outs> | ||
72 | defMultiplier :: Int | ||
73 | defMultiplier = 2 | ||
74 | |||
75 | -- TODO why 98? | ||
76 | defMaxPacketSize :: Int | ||
77 | defMaxPacketSize = 98 | ||
78 | |||
79 | -- | Manager configuration. | ||
80 | data 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. | ||
109 | instance Default Options where | ||
110 | def = Options | ||
111 | { optMaxPacketSize = defMaxPacketSize | ||
112 | , optMinTimeout = defMinTimeout | ||
113 | , optMaxTimeout = defMaxTimeout | ||
114 | , optMultiplier = defMultiplier | ||
115 | } | ||
116 | |||
117 | checkOptions :: Options -> IO () | ||
118 | checkOptions 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 | |||
139 | type ConnectionCache = Map SockAddr Connection | ||
140 | |||
141 | type PendingResponse = MVar (Either RpcException Response) | ||
142 | type PendingTransactions = Map TransactionId PendingResponse | ||
143 | type PendingQueries = Map SockAddr PendingTransactions | ||
144 | |||
145 | -- | UDP tracker manager. | ||
146 | data 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 | |||
155 | initManager :: Options -> IO Manager | ||
156 | initManager opts = Manager opts | ||
157 | <$> socket AF_INET Datagram defaultProtocol | ||
158 | <*> newIORef M.empty | ||
159 | <*> newMVar M.empty | ||
160 | <*> newEmptyMVar | ||
161 | |||
162 | unblockAll :: PendingQueries -> IO () | ||
163 | unblockAll m = traverse (traverse unblockCall) m >> return () | ||
164 | where | ||
165 | unblockCall ares = putMVar ares (Left ManagerClosed) | ||
166 | |||
167 | resetState :: Manager -> IO () | ||
168 | resetState 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'. | ||
181 | newManager :: Options -> IO Manager | ||
182 | newManager 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. | ||
191 | closeManager :: Manager -> IO () | ||
192 | closeManager 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'. | ||
200 | withManager :: Options -> (Manager -> IO a) -> IO a | ||
201 | withManager opts = bracket (newManager opts) closeManager | ||
202 | |||
203 | {----------------------------------------------------------------------- | ||
204 | -- Exceptions | ||
205 | -----------------------------------------------------------------------} | ||
206 | |||
207 | data 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 | |||
233 | instance Exception RpcException | ||
234 | |||
235 | {----------------------------------------------------------------------- | ||
236 | -- Host Addr resolution | ||
237 | -----------------------------------------------------------------------} | ||
238 | |||
239 | setPort :: PortNumber -> SockAddr -> SockAddr | ||
240 | setPort p (SockAddrInet _ h) = SockAddrInet p h | ||
241 | setPort p (SockAddrInet6 _ f h s) = SockAddrInet6 p f h s | ||
242 | setPort _ addr = addr | ||
243 | |||
244 | resolveURI :: URI -> IO SockAddr | ||
245 | resolveURI 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 | ||
251 | resolveURI _ = throwIO HostUnknown | ||
252 | |||
253 | -- TODO caching? | ||
254 | getTrackerAddr :: Manager -> URI -> IO SockAddr | ||
255 | getTrackerAddr _ uri | ||
256 | | uriScheme uri == "udp:" = resolveURI uri | ||
257 | | otherwise = throwIO (UnrecognizedScheme (uriScheme uri)) | ||
258 | |||
259 | {----------------------------------------------------------------------- | ||
260 | Connection | ||
261 | -----------------------------------------------------------------------} | ||
262 | |||
263 | connectionLifetime :: NominalDiffTime | ||
264 | connectionLifetime = 60 | ||
265 | |||
266 | data Connection = Connection | ||
267 | { connectionId :: ConnectionId | ||
268 | , connectionTimestamp :: UTCTime | ||
269 | } deriving Show | ||
270 | |||
271 | -- placeholder for the first 'connect' | ||
272 | initialConnection :: Connection | ||
273 | initialConnection = Connection initialConnectionId (posixSecondsToUTCTime 0) | ||
274 | |||
275 | establishedConnection :: ConnectionId -> IO Connection | ||
276 | establishedConnection cid = Connection cid <$> getCurrentTime | ||
277 | |||
278 | isExpired :: Connection -> IO Bool | ||
279 | isExpired 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. | ||
293 | firstUnused :: SockAddr -> TransactionId -> PendingQueries -> TransactionId | ||
294 | firstUnused 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 | |||
317 | register :: SockAddr -> TransactionId -> PendingResponse | ||
318 | -> PendingQueries -> PendingQueries | ||
319 | register 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 | |||
324 | unregister :: SockAddr -> TransactionId | ||
325 | -> PendingQueries -> PendingQueries | ||
326 | unregister 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. | ||
335 | allocTransaction :: Manager -> SockAddr -> PendingResponse -> IO TransactionId | ||
336 | allocTransaction 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. | ||
343 | commitTransaction :: Manager -> SockAddr -> TransactionId -> Response -> IO () | ||
344 | commitTransaction 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. | ||
353 | cancelTransaction :: Manager -> SockAddr -> TransactionId -> IO () | ||
354 | cancelTransaction Manager {..} addr tid = | ||
355 | modifyMVarMasked_ pendingResps $ \m -> | ||
356 | return $ unregister addr tid m | ||
357 | |||
358 | -- | Handle responses from trackers. | ||
359 | listen :: Manager -> IO () | ||
360 | listen 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. | ||
369 | transaction :: Manager -> SockAddr -> Connection -> Request -> IO Response | ||
370 | transaction 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 | |||
385 | connect :: Manager -> SockAddr -> Connection -> IO ConnectionId | ||
386 | connect 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 | |||
393 | newConnection :: Manager -> SockAddr -> IO Connection | ||
394 | newConnection m addr = do | ||
395 | connId <- connect m addr initialConnection | ||
396 | establishedConnection connId | ||
397 | |||
398 | refreshConnection :: Manager -> SockAddr -> Connection -> IO Connection | ||
399 | refreshConnection 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 | |||
408 | withCache :: Manager -> SockAddr | ||
409 | -> (Maybe Connection -> IO Connection) -> IO Connection | ||
410 | withCache 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 | |||
416 | getConnection :: Manager -> SockAddr -> IO Connection | ||
417 | getConnection mgr addr = withCache mgr addr $ | ||
418 | maybe (newConnection mgr addr) (refreshConnection mgr addr) | ||
419 | |||
420 | {----------------------------------------------------------------------- | ||
421 | -- RPC | ||
422 | -----------------------------------------------------------------------} | ||
423 | |||
424 | retransmission :: Options -> IO a -> IO a | ||
425 | retransmission 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 | |||
433 | queryTracker :: Manager -> URI -> Request -> IO Response | ||
434 | queryTracker 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'. | ||
441 | announce :: Manager -> URI -> AnnounceQuery -> IO AnnounceInfo | ||
442 | announce 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'. | ||
449 | scrape :: Manager -> URI -> ScrapeQuery -> IO ScrapeInfo | ||
450 | scrape 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 #-} | ||
14 | module 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 | |||
45 | import Control.Applicative | ||
46 | import Control.Exception | ||
47 | import Control.Concurrent | ||
48 | import Control.Concurrent.Chan.Split as CS | ||
49 | import Control.Monad | ||
50 | import Data.Default | ||
51 | import Data.Fixed | ||
52 | import Data.Foldable as F | ||
53 | import Data.IORef | ||
54 | import Data.List as L | ||
55 | import Data.Maybe | ||
56 | import Data.Time | ||
57 | import Data.Traversable | ||
58 | import Network.URI | ||
59 | |||
60 | import Data.Torrent | ||
61 | import Network.Address | ||
62 | import Network.BitTorrent.Internal.Cache | ||
63 | import Network.BitTorrent.Internal.Types | ||
64 | import Network.BitTorrent.Tracker.List as TL | ||
65 | import Network.BitTorrent.Tracker.Message | ||
66 | import Network.BitTorrent.Tracker.RPC as RPC | ||
67 | |||
68 | {----------------------------------------------------------------------- | ||
69 | -- Single tracker session | ||
70 | -----------------------------------------------------------------------} | ||
71 | |||
72 | -- | Status of this client. | ||
73 | data 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. | ||
80 | instance Default Status where | ||
81 | def = Paused | ||
82 | |||
83 | -- | Tracker session starts with scrape unknown. | ||
84 | instance Default LastScrape where | ||
85 | def = LastScrape Nothing Nothing | ||
86 | |||
87 | data 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. | ||
96 | data 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. | ||
108 | instance Default TrackerSession where | ||
109 | def = TrackerSession Nothing def def | ||
110 | |||
111 | -- | Do we need to notify this /specific/ tracker? | ||
112 | needNotify :: AnnounceEvent -> Maybe Status -> Maybe Bool | ||
113 | needNotify Started Nothing = Just True | ||
114 | needNotify Stopped Nothing = Just False | ||
115 | needNotify Completed Nothing = Just False | ||
116 | needNotify Started (Just Running) = Nothing | ||
117 | needNotify Stopped (Just Running) = Just True | ||
118 | needNotify Completed (Just Running) = Just True | ||
119 | needNotify Started (Just Paused ) = Just True | ||
120 | needNotify Stopped (Just Paused ) = Just False | ||
121 | needNotify Completed (Just Paused ) = Just True | ||
122 | |||
123 | -- | Client status after event announce succeed. | ||
124 | nextStatus :: AnnounceEvent -> Maybe Status | ||
125 | nextStatus Started = Just Running | ||
126 | nextStatus Stopped = Just Paused | ||
127 | nextStatus Completed = Nothing -- must keep previous status | ||
128 | |||
129 | seconds :: Int -> NominalDiffTime | ||
130 | seconds n = realToFrac (toEnum n :: Uni) | ||
131 | |||
132 | cachePeers :: AnnounceInfo -> IO (Cached [PeerAddr IP]) | ||
133 | cachePeers AnnounceInfo {..} = | ||
134 | newCached (seconds respInterval) | ||
135 | (seconds (fromMaybe respInterval respMinInterval)) | ||
136 | (getPeerList respPeers) | ||
137 | |||
138 | cacheScrape :: AnnounceInfo -> IO (Cached LastScrape) | ||
139 | cacheScrape 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. | ||
148 | notifyTo :: Manager -> Session -> AnnounceEvent | ||
149 | -> TierEntry TrackerSession -> IO TrackerSession | ||
150 | notifyTo 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. | ||
169 | data 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 | |||
185 | instance 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'. | ||
199 | newSession :: InfoHash -> TrackerList () -> IO Session | ||
200 | newSession 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. | ||
215 | closeSession :: Manager -> Session -> IO () | ||
216 | closeSession 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'. | ||
225 | withSession :: Manager -> InfoHash -> TrackerList () | ||
226 | -> (Session -> IO ()) -> IO () | ||
227 | withSession 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'. | ||
231 | getStatus :: Session -> IO Status | ||
232 | getStatus Session {..} = readIORef sessionStatus | ||
233 | |||
234 | getSessionState :: Session -> IO [[TierEntry TrackerSession]] | ||
235 | getSessionState 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? | ||
239 | allNotify :: AnnounceEvent -> Bool | ||
240 | allNotify Started = False | ||
241 | allNotify Stopped = True | ||
242 | allNotify Completed = True | ||
243 | |||
244 | notifyAll :: Manager -> Session -> AnnounceEvent -> IO () | ||
245 | notifyAll 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. | ||
257 | notify :: Manager -> Session -> AnnounceEvent -> IO () | ||
258 | notify 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. | ||
267 | askPeers :: Manager -> Session -> IO [PeerAddr IP] | ||
268 | askPeers _mgr ses = do | ||
269 | list <- readMVar (sessionTrackers ses) | ||
270 | L.concat <$> collect (tryTakeData . trackerPeers) list | ||
271 | |||
272 | collect :: (a -> IO (Maybe b)) -> TrackerList a -> IO [b] | ||
273 | collect 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 | |||
287 | addTracker :: Session -> URI -> IO () | ||
288 | addTracker Session {..} uri = do | ||
289 | undefined | ||
290 | send sessionEvents (TrackerAdded uri) | ||
291 | |||
292 | removeTracker :: Manager -> Session -> URI -> IO () | ||
293 | removeTracker 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. | ||
301 | getTrackers :: Session -> IO [URI] | ||
302 | getTrackers = undefined | ||
303 | |||
304 | -- | Return trackers from torrent file and | ||
305 | getTrustedTrackers :: Session -> IO [URI] | ||
306 | getTrustedTrackers = 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 #-} | ||
4 | module 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 | |||
26 | import Control.Applicative | ||
27 | import Control.Monad as L | ||
28 | import Data.ByteString as BS | ||
29 | import Data.ByteString.Internal as BS | ||
30 | import Data.ByteString.Lazy as BL | ||
31 | import Data.ByteString.Lazy.Internal as BL | ||
32 | import Data.Default | ||
33 | import Data.Vector as V -- TODO use unboxed vector | ||
34 | import Foreign | ||
35 | import System.IO.MMap | ||
36 | |||
37 | import Data.Torrent | ||
38 | |||
39 | |||
40 | data FileEntry = FileEntry | ||
41 | { filePosition :: {-# UNPACK #-} !FileOffset | ||
42 | , fileBytes :: {-# UNPACK #-} !BS.ByteString | ||
43 | } deriving (Show, Eq) | ||
44 | |||
45 | type FileMap = Vector FileEntry | ||
46 | |||
47 | instance Default Mode where | ||
48 | def = ReadWriteEx | ||
49 | |||
50 | mmapFiles :: Mode -> FileLayout FileSize -> IO FileMap | ||
51 | mmapFiles 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 | |||
60 | unmapFiles :: FileMap -> IO () | ||
61 | unmapFiles = V.mapM_ unmapEntry | ||
62 | where | ||
63 | unmapEntry (FileEntry _ (PS fptr _ _)) = finalizeForeignPtr fptr | ||
64 | |||
65 | fromLazyByteString :: BL.ByteString -> FileMap | ||
66 | fromLazyByteString 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)/. | ||
73 | toLazyByteString :: FileMap -> BL.ByteString | ||
74 | toLazyByteString = V.foldr f Empty | ||
75 | where | ||
76 | f FileEntry {..} bs = Chunk fileBytes bs | ||
77 | |||
78 | -- | /O(1)/. | ||
79 | size :: FileMap -> FileOffset | ||
80 | size m | ||
81 | | V.null m = 0 | ||
82 | | FileEntry {..} <- V.unsafeLast m | ||
83 | = filePosition + fromIntegral (BS.length fileBytes) | ||
84 | |||
85 | bsearch :: FileOffset -> FileMap -> Maybe Int | ||
86 | bsearch 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)/. | ||
100 | drop :: FileOffset -> FileMap -> (FileSize, FileMap) | ||
101 | drop 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)/. | ||
107 | take :: FileSize -> FileMap -> (FileMap, FileSize) | ||
108 | take 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'. | ||
117 | unsafeReadBytes :: FileOffset -> FileSize -> FileMap -> BL.ByteString | ||
118 | unsafeReadBytes 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 | |||
123 | readBytes :: FileOffset -> FileSize -> FileMap -> IO BL.ByteString | ||
124 | readBytes 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 | |||
132 | bscpy :: BL.ByteString -> BL.ByteString -> IO () | ||
133 | bscpy (PS _ _ 0 `Chunk` dest_rest) src = bscpy dest_rest src | ||
134 | bscpy dest (PS _ _ 0 `Chunk` src_rest) = bscpy dest src_rest | ||
135 | bscpy (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) | ||
145 | bscpy _ _ = return () | ||
146 | |||
147 | writeBytes :: FileOffset -> BL.ByteString -> FileMap -> IO () | ||
148 | writeBytes 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 #-} | ||
18 | module 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 | |||
48 | import Control.Applicative | ||
49 | import Control.Exception | ||
50 | import Control.Monad as M | ||
51 | import Control.Monad.Trans | ||
52 | import Data.ByteString.Lazy as BL | ||
53 | import Data.Conduit as C | ||
54 | import Data.Conduit.Binary as C | ||
55 | import Data.Conduit.List as C | ||
56 | import Data.Typeable | ||
57 | |||
58 | import Data.Torrent | ||
59 | import Network.BitTorrent.Exchange.Bitfield as BF | ||
60 | import System.Torrent.FileMap as FM | ||
61 | |||
62 | |||
63 | -- | Some storage operations may throw an exception if misused. | ||
64 | data 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 | |||
77 | instance Exception StorageFailure | ||
78 | |||
79 | -- | Pieces store. | ||
80 | data 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 | -- | ||
92 | open :: Mode -> PieceSize -> FileLayout FileSize -> IO Storage | ||
93 | open 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. | ||
98 | openInfoDict :: Mode -> FilePath -> InfoDict -> IO Storage | ||
99 | openInfoDict mode rootPath InfoDict {..} = | ||
100 | open mode (piPieceLength idPieceInfo) (flatLayout rootPath idLayoutInfo) | ||
101 | |||
102 | -- | Unmaps all files forcefully. It is recommended but not required. | ||
103 | close :: Storage -> IO () | ||
104 | close Storage {..} = unmapFiles fileMap | ||
105 | |||
106 | -- | Normally you need to use 'Control.Monad.Trans.Resource.allocate'. | ||
107 | withStorage :: Mode -> PieceSize -> FileLayout FileSize | ||
108 | -> (Storage -> IO ()) -> IO () | ||
109 | withStorage m s l = bracket (open m s l) close | ||
110 | |||
111 | -- TODO allocateStorage? | ||
112 | |||
113 | -- | Count of pieces in the storage. | ||
114 | totalPieces :: Storage -> PieceCount | ||
115 | totalPieces Storage {..} = FM.size fileMap `sizeInBase` pieceLen | ||
116 | |||
117 | isValidIx :: PieceIx -> Storage -> Bool | ||
118 | isValidIx 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 | -- | ||
125 | writePiece :: Piece BL.ByteString -> Storage -> IO () | ||
126 | writePiece 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 | -- | ||
150 | readPiece :: PieceIx -> Storage -> IO (Piece BL.ByteString) | ||
151 | readPiece 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 | -- | ||
162 | hintRead :: PieceIx -> Storage -> IO () | ||
163 | hintRead _pix Storage {..} = return () | ||
164 | |||
165 | -- | Zero-copy version of readPiece. Can be used only with 'ReadOnly' | ||
166 | -- storages. | ||
167 | unsafeReadPiece :: PieceIx -> Storage -> IO (Piece BL.ByteString) | ||
168 | unsafeReadPiece 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. | ||
176 | sourceStorage :: Storage -> Source IO (Piece BL.ByteString) | ||
177 | sourceStorage 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'. | ||
188 | sinkStorage :: Storage -> Sink (Piece BL.ByteString) IO () | ||
189 | sinkStorage 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. | ||
195 | genPieceInfo :: Storage -> IO PieceInfo | ||
196 | genPieceInfo 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. | ||
201 | verifyPiece :: Storage -> PieceInfo -> PieceIx -> IO Bool | ||
202 | verifyPiece 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 | -- | ||
211 | getBitfield :: Storage -> PieceInfo -> IO Bitfield | ||
212 | getBitfield 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 #-} | ||
13 | module 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 | |||
27 | import Data.ByteString as BS | ||
28 | import Data.ByteString.Char8 as BC | ||
29 | import Data.Foldable | ||
30 | import Data.List as L | ||
31 | import Data.Map as M | ||
32 | import Data.Monoid | ||
33 | |||
34 | import Data.Torrent | ||
35 | |||
36 | |||
37 | -- | 'DirTree' is more convenient form of 'LayoutInfo'. | ||
38 | data 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. | ||
43 | build :: LayoutInfo -> DirTree () | ||
44 | build SingleFile {liFile = FileInfo {..}} = Dir | ||
45 | { children = M.singleton fiName (File fi) } | ||
46 | where | ||
47 | fi = FileInfo fiLength fiMD5Sum () | ||
48 | build 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. | ||
61 | lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a) | ||
62 | lookup [] t = Just t | ||
63 | lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m | ||
64 | = System.Torrent.Tree.lookup ps subTree | ||
65 | lookup _ _ = Nothing | ||
66 | |||
67 | -- | Lookup directory by path. | ||
68 | lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)] | ||
69 | lookupDir 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. | ||
76 | fileNumber :: DirTree a -> Sum Int | ||
77 | fileNumber File {..} = Sum 1 | ||
78 | fileNumber Dir {..} = foldMap fileNumber children | ||
79 | |||
80 | -- | Get total count of directories in the directory and subdirectories. | ||
81 | dirNumber :: DirTree a -> Sum Int | ||
82 | dirNumber File {..} = Sum 0 | ||
83 | dirNumber 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 #-} | ||
3 | module 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 | |||
24 | import Control.Monad | ||
25 | import Network | ||
26 | import Data.Default | ||
27 | import Data.IORef | ||
28 | import Data.List as L | ||
29 | import Data.Maybe | ||
30 | import Options.Applicative | ||
31 | import System.Exit | ||
32 | import System.Environment | ||
33 | import System.IO.Unsafe | ||
34 | import Test.Hspec | ||
35 | |||
36 | import Data.Torrent | ||
37 | import Network.BitTorrent.Address (IP, PeerAddr (PeerAddr), genPeerId) | ||
38 | |||
39 | |||
40 | type ClientName = String | ||
41 | |||
42 | |||
43 | #if !MIN_VERSION_network(2,6,3) | ||
44 | instance Read PortNumber where | ||
45 | readsPrec = error "readsPrec" | ||
46 | #endif | ||
47 | |||
48 | data ClientOpts = ClientOpts | ||
49 | { peerPort :: PortNumber -- tcp port | ||
50 | , nodePort :: PortNumber -- udp port | ||
51 | } | ||
52 | |||
53 | instance Default ClientOpts where | ||
54 | def = ClientOpts | ||
55 | { peerPort = 6881 | ||
56 | , nodePort = 6881 | ||
57 | } | ||
58 | |||
59 | defRemoteOpts :: ClientOpts | ||
60 | defRemoteOpts = def | ||
61 | |||
62 | defThisOpts :: ClientOpts | ||
63 | defThisOpts = def | ||
64 | { peerPort = 6882 | ||
65 | , nodePort = 6882 | ||
66 | } | ||
67 | |||
68 | clientOptsParser :: Parser ClientOpts | ||
69 | clientOptsParser = 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 | |||
83 | data EnvOpts = EnvOpts | ||
84 | { testClient :: Maybe ClientName | ||
85 | , testTorrents :: [FilePath] | ||
86 | , remoteOpts :: ClientOpts | ||
87 | , thisOpts :: ClientOpts | ||
88 | } | ||
89 | |||
90 | instance Default EnvOpts where | ||
91 | def = EnvOpts | ||
92 | { testClient = Just "rtorrent" | ||
93 | , testTorrents = ["testfile.torrent"] | ||
94 | , remoteOpts = defRemoteOpts | ||
95 | , thisOpts = defThisOpts | ||
96 | } | ||
97 | |||
98 | findConflicts :: EnvOpts -> [String] | ||
99 | findConflicts 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 | |||
106 | envOptsParser :: Parser EnvOpts | ||
107 | envOptsParser = 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 | |||
117 | envOptsInfo :: ParserInfo EnvOpts | ||
118 | envOptsInfo = 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 | ||
126 | envOptsRef :: IORef EnvOpts | ||
127 | envOptsRef = unsafePerformIO (newIORef def) | ||
128 | |||
129 | -- | Should be used from spec items. | ||
130 | getEnvOpts :: IO EnvOpts | ||
131 | getEnvOpts = readIORef envOptsRef | ||
132 | |||
133 | getThisOpts :: IO ClientOpts | ||
134 | getThisOpts = thisOpts <$> getEnvOpts | ||
135 | |||
136 | -- | Return 'Nothing' if remote client is not running. | ||
137 | getRemoteOpts :: IO (Maybe ClientOpts) | ||
138 | getRemoteOpts = do | ||
139 | EnvOpts {..} <- getEnvOpts | ||
140 | return $ const remoteOpts <$> testClient | ||
141 | |||
142 | withRemote :: (ClientOpts -> Expectation) -> Expectation | ||
143 | withRemote action = do | ||
144 | mopts <- getRemoteOpts | ||
145 | case mopts of | ||
146 | Nothing -> pendingWith "Remote client isn't running" | ||
147 | Just opts -> action opts | ||
148 | |||
149 | withRemoteAddr :: (PeerAddr IP -> Expectation) -> Expectation | ||
150 | withRemoteAddr action = do | ||
151 | withRemote $ \ ClientOpts {..} -> | ||
152 | action (PeerAddr Nothing "0.0.0.0" peerPort) | ||
153 | |||
154 | getMyAddr :: IO (PeerAddr (Maybe IP)) | ||
155 | getMyAddr = do | ||
156 | ClientOpts {..} <- getThisOpts | ||
157 | pid <- genPeerId | ||
158 | return $ PeerAddr (Just pid) Nothing peerPort | ||
159 | |||
160 | getTestTorrent :: IO Torrent | ||
161 | getTestTorrent = 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. | ||
170 | getOpts :: IO (EnvOpts, [String]) | ||
171 | getOpts = 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 #-} | ||
5 | module Data.TorrentSpec (spec) where | ||
6 | import Control.Applicative | ||
7 | import Data.BEncode | ||
8 | import Data.ByteString as BS | ||
9 | import Data.ByteString.Lazy as BL | ||
10 | import Data.Convertible | ||
11 | import Data.Maybe | ||
12 | import Data.Monoid | ||
13 | import Data.Time | ||
14 | import Network.URI | ||
15 | import System.FilePath | ||
16 | import System.Posix.Types | ||
17 | import Test.Hspec | ||
18 | import Test.QuickCheck | ||
19 | import Test.QuickCheck.Instances () | ||
20 | |||
21 | import Data.Torrent | ||
22 | import Network.BitTorrent.CoreSpec () | ||
23 | |||
24 | |||
25 | pico :: Gen (Maybe NominalDiffTime) | ||
26 | pico = oneof | ||
27 | [ pure Nothing | ||
28 | , (Just . fromIntegral) <$> (arbitrary :: Gen Int) | ||
29 | ] | ||
30 | |||
31 | instance Arbitrary COff where | ||
32 | arbitrary = fromIntegral <$> (arbitrary :: Gen Int) | ||
33 | |||
34 | instance Arbitrary URIAuth where | ||
35 | arbitrary = URIAuth <$> arbitrary <*> arbitrary <*> arbitrary | ||
36 | |||
37 | instance Arbitrary URI where | ||
38 | arbitrary | ||
39 | = pure $ fromJust $ parseURI "http://ietf.org/1737.txt?a=1&b=h#123" | ||
40 | |||
41 | instance Arbitrary InfoHash where | ||
42 | arbitrary = do | ||
43 | bs <- BS.pack <$> vectorOf 20 arbitrary | ||
44 | pure $ either (const (error "arbitrary infohash")) id $ safeConvert bs | ||
45 | |||
46 | instance Arbitrary a => Arbitrary (FileInfo a) where | ||
47 | arbitrary = FileInfo <$> arbitrary <*> arbitrary <*> arbitrary | ||
48 | |||
49 | instance Arbitrary LayoutInfo where | ||
50 | arbitrary = oneof | ||
51 | [ SingleFile <$> arbitrary | ||
52 | , MultiFile <$> arbitrary <*> arbitrary | ||
53 | ] | ||
54 | |||
55 | instance Arbitrary a => Arbitrary (Piece a) where | ||
56 | arbitrary = Piece <$> arbitrary <*> arbitrary | ||
57 | |||
58 | instance Arbitrary HashList where | ||
59 | arbitrary = HashList <$> arbitrary | ||
60 | |||
61 | instance Arbitrary PieceInfo where | ||
62 | arbitrary = PieceInfo <$> arbitrary <*> arbitrary | ||
63 | |||
64 | instance Arbitrary InfoDict where | ||
65 | arbitrary = infoDictionary <$> arbitrary <*> arbitrary <*> arbitrary | ||
66 | |||
67 | instance Arbitrary Torrent where | ||
68 | arbitrary = Torrent <$> arbitrary | ||
69 | <*> arbitrary <*> arbitrary <*> arbitrary | ||
70 | <*> pico <*> arbitrary <*> arbitrary | ||
71 | <*> arbitrary | ||
72 | <*> arbitrary <*> pure Nothing <*> arbitrary | ||
73 | |||
74 | instance Arbitrary Magnet where | ||
75 | arbitrary = Magnet <$> arbitrary <*> arbitrary | ||
76 | <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary | ||
77 | <*> arbitrary <*> arbitrary <*> pure mempty | ||
78 | |||
79 | type TestPair = (FilePath, String) | ||
80 | |||
81 | -- TODO add a few more torrents here | ||
82 | torrentList :: [TestPair] | ||
83 | torrentList = | ||
84 | [ ( "res" </> "dapper-dvd-amd64.iso.torrent" | ||
85 | , "0221caf96aa3cb94f0f58d458e78b0fc344ad8bf") | ||
86 | ] | ||
87 | |||
88 | infohashSpec :: (FilePath, String) -> Spec | ||
89 | infohashSpec (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 | |||
95 | magnetEncoding :: Magnet -> IO () | ||
96 | magnetEncoding m = parseMagnet (renderMagnet m) `shouldBe` Just m | ||
97 | |||
98 | data T a = T | ||
99 | |||
100 | prop_properBEncode :: Show a => BEncode a => Eq a | ||
101 | => T a -> a -> IO () | ||
102 | prop_properBEncode _ expected = actual `shouldBe` Right expected | ||
103 | where | ||
104 | actual = decode $ BL.toStrict $ encode expected | ||
105 | |||
106 | spec :: Spec | ||
107 | spec = 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 #-} | ||
2 | module Main where | ||
3 | import Control.Exception | ||
4 | import Control.Monad | ||
5 | import Data.Functor | ||
6 | import Data.Maybe | ||
7 | import System.Directory | ||
8 | import System.Exit | ||
9 | import System.Environment | ||
10 | import System.FilePath | ||
11 | import System.Process | ||
12 | import Text.Printf | ||
13 | import Test.Hspec | ||
14 | |||
15 | import Config | ||
16 | import qualified Spec as Generated | ||
17 | |||
18 | |||
19 | type Command = String | ||
20 | type Descr = (ClientName, ClientOpts -> FilePath -> Command) | ||
21 | |||
22 | torrents :: [FilePath] | ||
23 | torrents = | ||
24 | [ "dapper-dvd-amd64-iso.torrent" | ||
25 | , "pkg.torrent" | ||
26 | , "testfile.torrent" | ||
27 | ] | ||
28 | |||
29 | rtorrentSessionDir :: String | ||
30 | rtorrentSessionDir = "rtorrent-sessiondir" | ||
31 | |||
32 | sessionName :: String -- screen session name | ||
33 | sessionName = "bittorrent-testsuite" | ||
34 | |||
35 | tmpDir :: FilePath | ||
36 | tmpDir = "res" | ||
37 | |||
38 | clients :: [Descr] | ||
39 | clients = | ||
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 | |||
48 | setupEnv :: EnvOpts -> IO (Maybe ()) | ||
49 | setupEnv 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 | |||
72 | terminateEnv :: IO () | ||
73 | terminateEnv = 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 | |||
80 | runTestSuite :: [String] -> IO ExitCode | ||
81 | runTestSuite 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 | |||
85 | withEnv :: EnvOpts -> IO a -> IO a | ||
86 | withEnv opts action = bracket (setupEnv opts) terminate (const action) | ||
87 | where | ||
88 | terminate running = do | ||
89 | when (isJust running) $ do | ||
90 | terminateEnv | ||
91 | |||
92 | main :: IO () | ||
93 | main = 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 @@ | |||
1 | module Network.BitTorrent.Client.HandleSpec (spec) where | ||
2 | import Data.Default | ||
3 | import Test.Hspec | ||
4 | |||
5 | import Data.Torrent | ||
6 | import Network.BitTorrent.Client | ||
7 | import Network.BitTorrent.Client.Handle | ||
8 | |||
9 | data_dir :: FilePath | ||
10 | data_dir = "data" | ||
11 | |||
12 | spec :: Spec | ||
13 | spec = 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 #-} | ||
3 | module Network.BitTorrent.CoreSpec (spec) where | ||
4 | import Control.Applicative | ||
5 | import Data.BEncode as BE | ||
6 | import Data.ByteString.Lazy as BL | ||
7 | import Data.IP | ||
8 | import Data.Serialize as S | ||
9 | import Data.String | ||
10 | import Data.Text.Encoding as T | ||
11 | import Data.Word | ||
12 | import Network | ||
13 | import Test.Hspec | ||
14 | import Test.QuickCheck | ||
15 | import Test.QuickCheck.Instances () | ||
16 | |||
17 | import Network.BitTorrent.Address | ||
18 | |||
19 | |||
20 | instance 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 | |||
28 | instance 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 | |||
40 | instance Arbitrary IP where | ||
41 | arbitrary = frequency | ||
42 | [ (1, IPv4 <$> arbitrary) | ||
43 | , (1, IPv6 <$> arbitrary) | ||
44 | ] | ||
45 | |||
46 | instance Arbitrary PortNumber where | ||
47 | arbitrary = fromIntegral <$> (arbitrary :: Gen Word16) | ||
48 | |||
49 | instance Arbitrary PeerId where | ||
50 | arbitrary = oneof | ||
51 | [ azureusStyle defaultClientId defaultVersionNumber | ||
52 | <$> (T.encodeUtf8 <$> arbitrary) | ||
53 | , shadowStyle 'X' defaultVersionNumber | ||
54 | <$> (T.encodeUtf8 <$> arbitrary) | ||
55 | ] | ||
56 | |||
57 | instance Arbitrary a => Arbitrary (PeerAddr a) where | ||
58 | arbitrary = PeerAddr <$> arbitrary <*> arbitrary <*> arbitrary | ||
59 | |||
60 | instance Arbitrary NodeId where | ||
61 | arbitrary = fromString <$> vector 20 | ||
62 | |||
63 | instance Arbitrary a => Arbitrary (NodeAddr a) where | ||
64 | arbitrary = NodeAddr <$> arbitrary <*> arbitrary | ||
65 | |||
66 | instance Arbitrary a => Arbitrary (NodeInfo a) where | ||
67 | arbitrary = NodeInfo <$> arbitrary <*> arbitrary | ||
68 | |||
69 | spec :: Spec | ||
70 | spec = 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 #-} | ||
2 | module Network.BitTorrent.DHT.MessageSpec (spec) where | ||
3 | import Control.Monad.Reader | ||
4 | import Control.Monad.Logger | ||
5 | import Control.Concurrent | ||
6 | import Data.BEncode as BE | ||
7 | import Data.ByteString.Lazy as BL | ||
8 | import Data.Default | ||
9 | import Data.List as L | ||
10 | import Data.Maybe | ||
11 | import Network.BitTorrent.Address | ||
12 | import Network.BitTorrent.DHT.Message | ||
13 | import qualified Network.KRPC as KRPC (def) | ||
14 | import Network.KRPC hiding (def) | ||
15 | import Network.Socket (PortNumber) | ||
16 | import Test.Hspec | ||
17 | import Test.QuickCheck | ||
18 | import System.Timeout | ||
19 | |||
20 | import Data.TorrentSpec () | ||
21 | import Network.BitTorrent.CoreSpec () | ||
22 | import Network.BitTorrent.DHT.TokenSpec () | ||
23 | |||
24 | -- Arbitrary queries and responses. | ||
25 | instance Arbitrary Ping where arbitrary = pure Ping | ||
26 | instance Arbitrary FindNode where arbitrary = FindNode <$> arbitrary | ||
27 | instance Arbitrary ip => Arbitrary (NodeFound ip) where arbitrary = NodeFound <$> arbitrary | ||
28 | instance Arbitrary GetPeers where arbitrary = GetPeers <$> arbitrary | ||
29 | instance Arbitrary ip => Arbitrary (GotPeers ip) where arbitrary = GotPeers <$> arbitrary <*> arbitrary | ||
30 | instance Arbitrary Announce where arbitrary = Announce <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary | ||
31 | instance Arbitrary Announced where arbitrary = pure Announced | ||
32 | instance Arbitrary x => Arbitrary (Query x) where arbitrary = Query <$> arbitrary <*> arbitrary <*> arbitrary | ||
33 | instance Arbitrary x => Arbitrary (Response x) where arbitrary = Response <$> arbitrary <*> arbitrary | ||
34 | |||
35 | instance MonadLogger IO where | ||
36 | monadLoggerLog _ _ _ _ = return () | ||
37 | |||
38 | remoteAddr :: SockAddr | ||
39 | remoteAddr = SockAddrInet 6881 (256 * 256 * 256 + 127) | ||
40 | |||
41 | thisAddr :: SockAddr | ||
42 | thisAddr = SockAddrInet 60000 (256 * 256 * 256 + 127) | ||
43 | |||
44 | thisPort :: PortNumber | ||
45 | thisPort = 60001 | ||
46 | |||
47 | rpc :: ReaderT (Manager IO) IO a -> IO a | ||
48 | rpc action = do | ||
49 | withManager KRPC.def thisAddr [] $ runReaderT $ do | ||
50 | listen | ||
51 | action | ||
52 | |||
53 | isQueryError :: QueryFailure -> Bool | ||
54 | isQueryError _ = True | ||
55 | |||
56 | prop_bencode :: Eq a => Show a => BEncode a => a -> Expectation | ||
57 | prop_bencode x = BE.decode (BL.toStrict (BE.encode x)) `shouldBe` Right x | ||
58 | |||
59 | retry :: Int -> IO (Maybe a) -> IO (Maybe a) | ||
60 | retry 0 _ = return Nothing | ||
61 | retry n a = do | ||
62 | res <- a | ||
63 | case res of | ||
64 | Just _ -> return res | ||
65 | Nothing -> threadDelay (100 * 1000) >> retry (n-1) a | ||
66 | |||
67 | spec :: Spec | ||
68 | spec = 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 #-} | ||
2 | module Network.BitTorrent.DHT.QuerySpec (spec) where | ||
3 | import Control.Applicative | ||
4 | import Control.Exception | ||
5 | import Control.Monad.Reader | ||
6 | import Data.Conduit as C | ||
7 | import Data.Conduit.List as CL | ||
8 | import Data.Default | ||
9 | import Data.List as L | ||
10 | import Test.Hspec | ||
11 | |||
12 | import Network.BitTorrent.Address | ||
13 | import Network.BitTorrent.DHT | ||
14 | import Network.BitTorrent.DHT.Session | ||
15 | import Network.BitTorrent.DHT.Query | ||
16 | |||
17 | import Network.BitTorrent.DHT.TestData | ||
18 | |||
19 | |||
20 | myAddr :: NodeAddr IPv4 | ||
21 | myAddr = "0.0.0.0:0" | ||
22 | |||
23 | nullLogger :: LogFun | ||
24 | nullLogger _ _ _ _ = return () | ||
25 | |||
26 | --simpleLogger :: LogFun | ||
27 | --simpleLogger _ t _ _ = print t | ||
28 | |||
29 | simpleDHT :: [NodeHandler IPv4] -> DHT IPv4 a -> IO a | ||
30 | simpleDHT hs m = | ||
31 | bracket (newNode hs def myAddr nullLogger Nothing) closeNode $ \ node -> | ||
32 | runDHT node m | ||
33 | |||
34 | getBootInfo :: IO (NodeInfo IPv4) | ||
35 | getBootInfo = do | ||
36 | startAddr <- resolveHostName (L.head defaultBootstrapNodes) | ||
37 | simpleDHT [] $ fmap fst (pingQ startAddr) | ||
38 | |||
39 | spec :: Spec | ||
40 | spec = 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 #-} | ||
3 | module Network.BitTorrent.DHT.RoutingSpec (spec) where | ||
4 | import Control.Applicative | ||
5 | import Control.Monad.State | ||
6 | import Data.Default | ||
7 | import Data.List as L | ||
8 | import Data.Maybe | ||
9 | import Test.Hspec | ||
10 | import Test.QuickCheck | ||
11 | |||
12 | import Network.BitTorrent.Address | ||
13 | import Network.BitTorrent.DHT.Routing as T | ||
14 | |||
15 | import Network.BitTorrent.CoreSpec hiding (spec) | ||
16 | |||
17 | |||
18 | type Network ip = [NodeAddr ip] | ||
19 | |||
20 | data Env ip = Env | ||
21 | { currentTime :: Timestamp | ||
22 | , network :: Network ip | ||
23 | } deriving Show | ||
24 | |||
25 | type Simulation ip = State (Env ip) | ||
26 | |||
27 | runSimulation :: Eq ip => Env ip -> Routing ip a -> Maybe a | ||
28 | runSimulation 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 | |||
34 | instance Arbitrary ip => Arbitrary (Env ip) where | ||
35 | arbitrary = Env <$> arbitrary <*> (vector nodeCount) | ||
36 | where | ||
37 | nodeCount = 1000 | ||
38 | |||
39 | instance (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 | |||
61 | spec :: Spec | ||
62 | spec = 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 #-} | ||
2 | module Network.BitTorrent.DHT.SessionSpec (spec) where | ||
3 | import Control.Applicative | ||
4 | import Control.Concurrent | ||
5 | import Control.Exception | ||
6 | import Control.Monad.Reader | ||
7 | import Control.Monad.Trans.Resource | ||
8 | import Data.Conduit.Lazy | ||
9 | import Data.Default | ||
10 | import Data.List as L | ||
11 | import Test.Hspec | ||
12 | import Test.QuickCheck | ||
13 | |||
14 | import Network.BitTorrent.Address | ||
15 | import Network.BitTorrent.DHT | ||
16 | import Network.BitTorrent.DHT.Message | ||
17 | import Network.BitTorrent.DHT.Session | ||
18 | import Network.BitTorrent.DHT.Query | ||
19 | |||
20 | import Data.TorrentSpec () | ||
21 | import Network.BitTorrent.CoreSpec () | ||
22 | import Network.BitTorrent.DHT.TokenSpec () | ||
23 | |||
24 | |||
25 | myAddr :: NodeAddr IPv4 | ||
26 | myAddr = "127.0.0.1:60000" | ||
27 | |||
28 | simpleDHT :: DHT IPv4 a -> IO a | ||
29 | simpleDHT m = | ||
30 | bracket (newNode defaultHandlers def myAddr nullLogger Nothing) closeNode $ \ node -> | ||
31 | runDHT node m | ||
32 | |||
33 | isRight :: Either a b -> Bool | ||
34 | isRight (Left _) = False | ||
35 | isRight (Right _) = True | ||
36 | |||
37 | isLeft :: Either a b -> Bool | ||
38 | isLeft = not . isRight | ||
39 | |||
40 | nullLogger :: LogFun | ||
41 | nullLogger _ _ _ _ = return () | ||
42 | |||
43 | spec :: Spec | ||
44 | spec = 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 @@ | |||
1 | module Network.BitTorrent.DHT.TestData | ||
2 | ( TestEntry (..) | ||
3 | , testTorrents | ||
4 | ) where | ||
5 | |||
6 | import Data.Torrent | ||
7 | |||
8 | data TestEntry = TestEntry | ||
9 | { entryName :: String | ||
10 | , entryHash :: InfoHash | ||
11 | , entryPeers :: Int -- ^ approximate number of peers, may change with time | ||
12 | } | ||
13 | |||
14 | testTorrents :: [TestEntry] | ||
15 | testTorrents = | ||
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 #-} | ||
3 | module Network.BitTorrent.DHT.TokenSpec (spec) where | ||
4 | import Control.Applicative | ||
5 | import Data.List as L | ||
6 | import Data.String | ||
7 | import Test.Hspec | ||
8 | import Test.QuickCheck | ||
9 | |||
10 | import Network.BitTorrent.Address | ||
11 | import Network.BitTorrent.CoreSpec () | ||
12 | import Network.BitTorrent.DHT.Token as T | ||
13 | |||
14 | |||
15 | instance Arbitrary Token where | ||
16 | arbitrary = fromString <$> arbitrary | ||
17 | |||
18 | instance Arbitrary TokenMap where | ||
19 | arbitrary = tokens <$> arbitrary | ||
20 | |||
21 | repeatN :: Int -> (a -> a) -> (a -> a) | ||
22 | repeatN n f = L.foldr (.) id $ L.replicate n f | ||
23 | |||
24 | spec :: Spec | ||
25 | spec = 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 @@ | |||
1 | module Network.BitTorrent.DHTSpec (spec) where | ||
2 | import Control.Exception | ||
3 | import Control.Monad | ||
4 | import Data.Default | ||
5 | import Data.List as L | ||
6 | import Test.Hspec | ||
7 | import System.Timeout | ||
8 | |||
9 | import Data.Torrent | ||
10 | import Network.BitTorrent.DHT | ||
11 | |||
12 | |||
13 | partialBootstrapTimeout :: Int | ||
14 | partialBootstrapTimeout = 10 * 1000000 | ||
15 | |||
16 | opts :: Options | ||
17 | opts = def { optBucketCount = 1 } | ||
18 | |||
19 | -- NOTE to shorten test cases run time include only "good" infohashes | ||
20 | -- with many nodes | ||
21 | existingInfoHashes :: [InfoHash] | ||
22 | existingInfoHashes = | ||
23 | [ | ||
24 | ] | ||
25 | |||
26 | -- TODO use Test.Hspec.parallel | ||
27 | |||
28 | spec :: Spec | ||
29 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Exchange.BitfieldSpec (spec) where | ||
3 | import Control.Applicative | ||
4 | import Data.ByteString.Arbitrary | ||
5 | import Test.Hspec | ||
6 | import Test.QuickCheck | ||
7 | |||
8 | import Network.BitTorrent.Exchange.Bitfield | ||
9 | |||
10 | instance Arbitrary Bitfield where | ||
11 | arbitrary = fromBitmap . fromABS <$> arbitrary | ||
12 | |||
13 | spec :: Spec | ||
14 | spec = 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 @@ | |||
1 | module Network.BitTorrent.Exchange.BlockSpec (spec) where | ||
2 | import Control.Applicative | ||
3 | import Control.Exception | ||
4 | import Data.Maybe | ||
5 | import Test.Hspec | ||
6 | import Test.QuickCheck | ||
7 | import Test.QuickCheck.Instances () | ||
8 | |||
9 | import Network.BitTorrent.Exchange.Block as Block | ||
10 | |||
11 | |||
12 | instance Arbitrary a => Arbitrary (Block a) where | ||
13 | arbitrary = Block <$> arbitrary <*> arbitrary <*> arbitrary | ||
14 | |||
15 | instance Arbitrary BlockIx where | ||
16 | arbitrary = BlockIx <$> arbitrary <*> arbitrary <*> arbitrary | ||
17 | |||
18 | instance Arbitrary Bucket where | ||
19 | arbitrary = do | ||
20 | s <- arbitrary `suchThat` (> 0) | ||
21 | chunks <- arbitrary | ||
22 | return $ Block.fromList s chunks | ||
23 | |||
24 | isSomeException :: SomeException -> Bool | ||
25 | isSomeException = const True | ||
26 | |||
27 | spec :: Spec | ||
28 | spec = 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 #-} | ||
3 | module Network.BitTorrent.Exchange.ConnectionSpec (spec) where | ||
4 | import Control.Applicative | ||
5 | import Control.Monad.Trans | ||
6 | import Data.Default | ||
7 | import Test.Hspec | ||
8 | import Test.QuickCheck | ||
9 | |||
10 | import Data.Torrent | ||
11 | import Network.BitTorrent.Address | ||
12 | import Network.BitTorrent.Exchange.Connection | ||
13 | import Network.BitTorrent.Exchange.Message | ||
14 | |||
15 | import Config | ||
16 | import Network.BitTorrent.Exchange.MessageSpec () | ||
17 | |||
18 | nullSession :: InfoHash -> PeerId -> SessionLink () | ||
19 | nullSession ih pid = SessionLink ih pid Nothing Nothing () | ||
20 | |||
21 | instance Arbitrary Options where | ||
22 | arbitrary = return def | ||
23 | |||
24 | instance Arbitrary ConnectionPrefs where | ||
25 | arbitrary = ConnectionPrefs <$> arbitrary <*> pure def | ||
26 | <*> arbitrary <*> arbitrary | ||
27 | |||
28 | withWire :: ConnectionPrefs -> Wire () () -> IO () | ||
29 | withWire 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 | |||
38 | spec :: Spec | ||
39 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Exchange.DownloadSpec (spec) where | ||
3 | import Control.Concurrent | ||
4 | import Data.ByteString as BS | ||
5 | import Data.ByteString.Lazy as BL | ||
6 | import Test.Hspec | ||
7 | import Test.QuickCheck | ||
8 | |||
9 | import Data.BEncode as BE | ||
10 | import Data.Torrent as Torrent | ||
11 | import Network.BitTorrent.Address | ||
12 | import Network.BitTorrent.Exchange.Download | ||
13 | import Network.BitTorrent.Exchange.Message | ||
14 | |||
15 | import Config | ||
16 | import Network.BitTorrent.CoreSpec () | ||
17 | |||
18 | |||
19 | placeholderAddr :: PeerAddr IP | ||
20 | placeholderAddr = "0.0.0.0:0" | ||
21 | |||
22 | chunkBy :: Int -> BS.ByteString -> [BS.ByteString] | ||
23 | chunkBy s bs | ||
24 | | BS.null bs = [] | ||
25 | | otherwise = BS.take s bs : chunkBy s (BS.drop s bs) | ||
26 | |||
27 | withUpdates :: Updates s a -> IO a | ||
28 | withUpdates 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 | |||
35 | simulateFetch :: InfoDict -> Updates s (Maybe InfoDict) | ||
36 | simulateFetch dict = undefined | ||
37 | |||
38 | spec :: Spec | ||
39 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Exchange.MessageSpec (spec) where | ||
3 | import Control.Applicative | ||
4 | import Control.Exception | ||
5 | import Data.ByteString as BS | ||
6 | import Data.List as L | ||
7 | import Data.Set as S | ||
8 | import Data.Serialize as S | ||
9 | import Data.String | ||
10 | import Test.Hspec | ||
11 | import Test.QuickCheck | ||
12 | |||
13 | import Data.TorrentSpec () | ||
14 | import Network.BitTorrent.Exchange.BitfieldSpec () | ||
15 | import Network.BitTorrent.CoreSpec () | ||
16 | import Network.BitTorrent.Address () | ||
17 | import Network.BitTorrent.Exchange.BlockSpec () | ||
18 | import Network.BitTorrent.Exchange.Message | ||
19 | |||
20 | instance Arbitrary Extension where | ||
21 | arbitrary = elements [minBound .. maxBound] | ||
22 | |||
23 | instance Arbitrary Caps where | ||
24 | arbitrary = toCaps <$> arbitrary | ||
25 | |||
26 | instance Arbitrary ExtendedExtension where | ||
27 | arbitrary = elements [minBound .. maxBound] | ||
28 | |||
29 | instance Arbitrary ExtendedCaps where | ||
30 | arbitrary = toCaps <$> arbitrary | ||
31 | |||
32 | instance Arbitrary ProtocolName where | ||
33 | arbitrary = fromString <$> (arbitrary `suchThat` ((200 <) . L.length)) | ||
34 | |||
35 | instance Arbitrary Handshake where | ||
36 | arbitrary = Handshake <$> arbitrary <*> arbitrary | ||
37 | <*> arbitrary <*> arbitrary | ||
38 | |||
39 | instance Arbitrary StatusUpdate where | ||
40 | arbitrary = frequency | ||
41 | [ (1, Choking <$> arbitrary) | ||
42 | , (1, Interested <$> arbitrary) | ||
43 | ] | ||
44 | |||
45 | instance Arbitrary Available where | ||
46 | arbitrary = frequency | ||
47 | [ (1, Have <$> arbitrary) | ||
48 | , (1, Bitfield <$> arbitrary) | ||
49 | ] | ||
50 | |||
51 | instance Arbitrary Transfer where | ||
52 | arbitrary = frequency | ||
53 | [ (1, Request <$> arbitrary) | ||
54 | , (1, Piece <$> arbitrary) | ||
55 | , (1, Cancel <$> arbitrary) | ||
56 | ] | ||
57 | |||
58 | instance 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 | |||
67 | instance 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 | |||
78 | spec :: Spec | ||
79 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Exchange.SessionSpec (spec) where | ||
3 | import Test.Hspec | ||
4 | |||
5 | import Data.Torrent | ||
6 | import Network.BitTorrent.Address | ||
7 | import Network.BitTorrent.Exchange.Session | ||
8 | |||
9 | import Config | ||
10 | |||
11 | |||
12 | nullLogger :: LogFun | ||
13 | nullLogger _ _ x _ = print x | ||
14 | |||
15 | simpleSession :: InfoDict -> (Session -> IO ()) -> IO () | ||
16 | simpleSession 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 | |||
24 | spec :: Spec | ||
25 | spec = 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 @@ | |||
1 | module Network.BitTorrent.Internal.CacheSpec (spec) where | ||
2 | import Test.Hspec | ||
3 | |||
4 | spec :: Spec | ||
5 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Internal.ProgressSpec (spec) where | ||
3 | import Control.Applicative | ||
4 | import Test.Hspec | ||
5 | import Test.QuickCheck | ||
6 | import Network.BitTorrent.Internal.Progress | ||
7 | |||
8 | |||
9 | instance Arbitrary Progress where | ||
10 | arbitrary = Progress <$> arbitrary <*> arbitrary <*> arbitrary | ||
11 | |||
12 | spec :: Spec | ||
13 | spec = 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 @@ | |||
1 | module Network.BitTorrent.Tracker.ListSpec (spec) where | ||
2 | import Control.Exception | ||
3 | import Data.Default | ||
4 | import Data.Foldable as F | ||
5 | import Data.List as L | ||
6 | import Data.Maybe | ||
7 | import Network.URI | ||
8 | import Test.Hspec | ||
9 | |||
10 | import Data.Torrent | ||
11 | import Network.BitTorrent.Tracker.List | ||
12 | import Network.BitTorrent.Tracker.RPC | ||
13 | |||
14 | |||
15 | uris :: [URI] | ||
16 | uris = fmap (fromJust . parseURI . renderURI) [1..10 :: Int] | ||
17 | where | ||
18 | renderURI n = "http://" ++ show n ++ ".org" | ||
19 | |||
20 | list :: TrackerList () | ||
21 | list = trackerList def { tAnnounceList = Just [uris] } | ||
22 | |||
23 | spec :: Spec | ||
24 | spec = 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 #-} | ||
5 | module Network.BitTorrent.Tracker.MessageSpec | ||
6 | ( spec | ||
7 | , arbitrarySample | ||
8 | ) where | ||
9 | |||
10 | import Control.Applicative | ||
11 | import Control.Exception | ||
12 | import Data.BEncode as BE | ||
13 | import Data.ByteString.Lazy as BL | ||
14 | import Data.List as L | ||
15 | import Data.Maybe | ||
16 | import Test.Hspec | ||
17 | import Test.QuickCheck | ||
18 | |||
19 | import Data.TorrentSpec () | ||
20 | import Network.BitTorrent.Internal.ProgressSpec () | ||
21 | import Network.BitTorrent.Address () | ||
22 | import Network.BitTorrent.Address () | ||
23 | |||
24 | import Network.BitTorrent.Tracker.Message as Message | ||
25 | import 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 | |||
34 | instance Arbitrary AnnounceEvent where | ||
35 | arbitrary = elements [minBound..maxBound] | ||
36 | |||
37 | instance Arbitrary AnnounceQuery where | ||
38 | arbitrary = AnnounceQuery | ||
39 | <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary | ||
40 | <*> arbitrary <*> arbitrary <*> arbitrary | ||
41 | |||
42 | instance 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 | |||
51 | instance Arbitrary AnnounceInfo where | ||
52 | arbitrary = AnnounceInfo | ||
53 | <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary | ||
54 | <*> arbitrary <*> arbitrary | ||
55 | |||
56 | arbitrarySample :: Arbitrary a => IO a | ||
57 | arbitrarySample = L.head <$> sample' arbitrary | ||
58 | |||
59 | zeroPeerId :: PeerAddr a -> PeerAddr a | ||
60 | zeroPeerId addr = addr { peerId = Nothing } | ||
61 | |||
62 | spec :: Spec | ||
63 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Tracker.RPC.HTTPSpec (spec) where | ||
3 | import Control.Monad | ||
4 | import Data.Default | ||
5 | import Data.List as L | ||
6 | import Test.Hspec | ||
7 | |||
8 | import Network.BitTorrent.Internal.Progress | ||
9 | import Network.BitTorrent.Tracker.Message as Message | ||
10 | import Network.BitTorrent.Tracker.RPC.HTTP | ||
11 | |||
12 | import Network.BitTorrent.Tracker.TestData | ||
13 | import Network.BitTorrent.Tracker.MessageSpec hiding (spec) | ||
14 | |||
15 | |||
16 | validateInfo :: AnnounceQuery -> AnnounceInfo -> Expectation | ||
17 | validateInfo _ (Message.Failure reason) = do | ||
18 | error $ "validateInfo: " ++ show reason | ||
19 | validateInfo AnnounceQuery {..} AnnounceInfo {..} = do | ||
20 | return () | ||
21 | -- case respComplete <|> respIncomplete of | ||
22 | -- Nothing -> return () | ||
23 | -- Just n -> n `shouldBe` L.length (getPeerList respPeers) | ||
24 | |||
25 | isUnrecognizedScheme :: RpcException -> Bool | ||
26 | isUnrecognizedScheme (RequestFailed _) = True | ||
27 | isUnrecognizedScheme _ = False | ||
28 | |||
29 | isNotResponding :: RpcException -> Bool | ||
30 | isNotResponding (RequestFailed _) = True | ||
31 | isNotResponding _ = False | ||
32 | |||
33 | spec :: Spec | ||
34 | spec = 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 #-} | ||
2 | module Network.BitTorrent.Tracker.RPC.UDPSpec (spec, rpcOpts) where | ||
3 | import Control.Concurrent | ||
4 | import Control.Concurrent.Async | ||
5 | import Control.Exception | ||
6 | import Control.Monad | ||
7 | import Data.Default | ||
8 | import Data.List as L | ||
9 | import Data.Maybe | ||
10 | import Test.Hspec | ||
11 | |||
12 | import Network.BitTorrent.Address | ||
13 | import Network.BitTorrent.Tracker.Message as Message | ||
14 | |||
15 | import Network.BitTorrent.Tracker.TestData | ||
16 | import Network.BitTorrent.Tracker.MessageSpec hiding (spec) | ||
17 | import Network.BitTorrent.Tracker.RPC.UDP | ||
18 | |||
19 | |||
20 | validateInfo :: AnnounceQuery -> AnnounceInfo -> Expectation | ||
21 | validateInfo _ Message.Failure {} = error "validateInfo: failure" | ||
22 | validateInfo 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. | ||
32 | rpcCount :: Int | ||
33 | rpcCount = 100 | ||
34 | |||
35 | rpcOpts :: Options | ||
36 | rpcOpts = def | ||
37 | { optMinTimeout = 1 | ||
38 | , optMaxTimeout = 10 | ||
39 | } | ||
40 | |||
41 | isTimeoutExpired :: RpcException -> Bool | ||
42 | isTimeoutExpired (TimeoutExpired _) = True | ||
43 | isTimeoutExpired _ = False | ||
44 | |||
45 | isSomeException :: SomeException -> Bool | ||
46 | isSomeException _ = True | ||
47 | |||
48 | isIOException :: IOException -> Bool | ||
49 | isIOException _ = True | ||
50 | |||
51 | spec :: Spec | ||
52 | spec = 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 #-} | ||
3 | module Network.BitTorrent.Tracker.RPCSpec (spec) where | ||
4 | import Control.Applicative | ||
5 | import Control.Monad | ||
6 | import Data.Default | ||
7 | import Data.List as L | ||
8 | import Test.Hspec | ||
9 | import Test.QuickCheck | ||
10 | |||
11 | import Network.BitTorrent.Tracker.RPC as RPC | ||
12 | |||
13 | import Network.BitTorrent.Tracker.TestData | ||
14 | import Network.BitTorrent.Tracker.MessageSpec hiding (spec) | ||
15 | import qualified Network.BitTorrent.Tracker.RPC.UDPSpec as UDP (rpcOpts) | ||
16 | |||
17 | |||
18 | instance Arbitrary SAnnounceQuery where | ||
19 | arbitrary = SAnnounceQuery <$> arbitrary <*> arbitrary | ||
20 | <*> arbitrary <*> arbitrary | ||
21 | |||
22 | rpcOpts :: Options | ||
23 | rpcOpts = def | ||
24 | { optUdpRPC = UDP.rpcOpts | ||
25 | } | ||
26 | |||
27 | matchUnrecognizedScheme :: String -> RpcException -> Bool | ||
28 | matchUnrecognizedScheme x (UnrecognizedScheme scheme) = x == scheme | ||
29 | matchUnrecognizedScheme _ _ = False | ||
30 | |||
31 | spec :: Spec | ||
32 | spec = 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 @@ | |||
1 | module Network.BitTorrent.Tracker.SessionSpec (spec) where | ||
2 | import Control.Monad | ||
3 | import Data.Default | ||
4 | import Data.List as L | ||
5 | import Test.Hspec | ||
6 | |||
7 | import Data.Torrent | ||
8 | import Network.BitTorrent.Tracker.Message | ||
9 | import Network.BitTorrent.Tracker.List | ||
10 | import Network.BitTorrent.Tracker.RPC | ||
11 | import Network.BitTorrent.Tracker.Session | ||
12 | |||
13 | import Config | ||
14 | |||
15 | testSession :: Bool -> (Manager -> Session -> IO ()) -> IO () | ||
16 | testSession 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 | |||
26 | spec :: Spec | ||
27 | spec = 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 #-} | ||
3 | module Network.BitTorrent.Tracker.TestData | ||
4 | ( TrackerEntry (..) | ||
5 | , isUdpTracker | ||
6 | , isHttpTracker | ||
7 | , trackers | ||
8 | , badTracker | ||
9 | ) where | ||
10 | |||
11 | import Data.Maybe | ||
12 | import Data.String | ||
13 | import Network.URI | ||
14 | |||
15 | import Data.Torrent | ||
16 | |||
17 | |||
18 | data 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 | |||
36 | isUdpTracker :: TrackerEntry -> Bool | ||
37 | isUdpTracker TrackerEntry {..} = uriScheme trackerURI == "udp:" | ||
38 | |||
39 | isHttpTracker :: TrackerEntry -> Bool | ||
40 | isHttpTracker TrackerEntry {..} = uriScheme trackerURI == "http:" | ||
41 | || uriScheme trackerURI == "https:" | ||
42 | |||
43 | instance IsString URI where | ||
44 | fromString str = fromMaybe err $ parseURI str | ||
45 | where | ||
46 | err = error $ "fromString: bad URI " ++ show str | ||
47 | |||
48 | trackerEntry :: URI -> TrackerEntry | ||
49 | trackerEntry uri = TrackerEntry | ||
50 | { trackerName = maybe "<unknown>" uriRegName (uriAuthority uri) | ||
51 | , trackerURI = uri | ||
52 | , tryAnnounce = False | ||
53 | , tryScraping = False | ||
54 | , hashList = Nothing | ||
55 | } | ||
56 | |||
57 | announceOnly :: String -> URI -> TrackerEntry | ||
58 | announceOnly name uri = (trackerEntry uri) | ||
59 | { trackerName = name | ||
60 | , tryAnnounce = True | ||
61 | } | ||
62 | |||
63 | announceScrape :: String -> URI -> TrackerEntry | ||
64 | announceScrape name uri = (announceOnly name uri) | ||
65 | { tryScraping = True | ||
66 | } | ||
67 | |||
68 | notWorking :: String -> URI -> TrackerEntry | ||
69 | notWorking name uri = (trackerEntry uri) | ||
70 | { trackerName = name | ||
71 | } | ||
72 | |||
73 | trackers :: [TrackerEntry] | ||
74 | trackers = | ||
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 | |||
92 | badTracker :: TrackerEntry | ||
93 | badTracker = 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 #-} | ||
3 | module Network.KRPC.MessageSpec (spec) where | ||
4 | import Control.Applicative | ||
5 | import Data.ByteString.Lazy as BL | ||
6 | import Test.Hspec | ||
7 | import Test.QuickCheck | ||
8 | import Test.QuickCheck.Instances () | ||
9 | |||
10 | import Data.BEncode as BE | ||
11 | import Network.KRPC.Message | ||
12 | |||
13 | instance Arbitrary ErrorCode where | ||
14 | arbitrary = arbitraryBoundedEnum | ||
15 | |||
16 | instance Arbitrary KError where | ||
17 | arbitrary = KError <$> arbitrary <*> arbitrary <*> arbitrary | ||
18 | |||
19 | instance Arbitrary KQuery where | ||
20 | arbitrary = KQuery <$> pure (BInteger 0) <*> arbitrary <*> arbitrary | ||
21 | |||
22 | instance Arbitrary KResponse where | ||
23 | -- TODO: Abitrary instance for ReflectedIP | ||
24 | arbitrary = KResponse <$> pure (BList []) <*> arbitrary <*> pure Nothing | ||
25 | |||
26 | instance Arbitrary KMessage where | ||
27 | arbitrary = frequency | ||
28 | [ (1, Q <$> arbitrary) | ||
29 | , (1, R <$> arbitrary) | ||
30 | , (1, E <$> arbitrary) | ||
31 | ] | ||
32 | |||
33 | spec :: Spec | ||
34 | spec = 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 #-} | ||
7 | module Network.KRPC.MethodSpec where | ||
8 | import Control.Applicative | ||
9 | import Data.BEncode | ||
10 | import Data.ByteString as BS | ||
11 | import Data.Typeable | ||
12 | import Network.KRPC | ||
13 | import Test.Hspec | ||
14 | |||
15 | |||
16 | data Ping = Ping | ||
17 | deriving (Show, Eq, Typeable) | ||
18 | |||
19 | instance BEncode Ping where | ||
20 | toBEncode Ping = toBEncode () | ||
21 | fromBEncode b = Ping <$ (fromBEncode b :: Result ()) | ||
22 | |||
23 | instance KRPC Ping Ping | ||
24 | |||
25 | ping :: Monad h => Handler h | ||
26 | ping = handler $ \ _ Ping -> return Ping | ||
27 | |||
28 | newtype Echo a = Echo a | ||
29 | deriving (Show, Eq, BEncode, Typeable) | ||
30 | |||
31 | echo :: Monad h => Handler h | ||
32 | echo = handler $ \ _ (Echo a) -> return (Echo (a :: ByteString)) | ||
33 | |||
34 | instance (Typeable a, BEncode a) => KRPC (Echo a) (Echo a) | ||
35 | |||
36 | spec :: Spec | ||
37 | spec = 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 #-} | ||
3 | module Network.KRPCSpec (spec) where | ||
4 | import Control.Monad.Logger | ||
5 | import Control.Monad.Reader | ||
6 | import Network.KRPC | ||
7 | import Network.KRPC.MethodSpec hiding (spec) | ||
8 | import Test.Hspec | ||
9 | |||
10 | servAddr :: SockAddr | ||
11 | servAddr = SockAddrInet 6000 (256 * 256 * 256 + 127) | ||
12 | |||
13 | handlers :: [Handler IO] | ||
14 | handlers = | ||
15 | [ handler $ \ _ Ping -> return Ping | ||
16 | , handler $ \ _ (Echo a) -> return (Echo (a :: Bool)) | ||
17 | , handler $ \ _ (Echo a) -> return (Echo (a :: Int)) | ||
18 | ] | ||
19 | |||
20 | instance MonadLogger IO where | ||
21 | monadLoggerLog _ _ _ _ = return () | ||
22 | |||
23 | opts :: Options | ||
24 | opts = def { optQueryTimeout = 1 } | ||
25 | |||
26 | spec :: Spec | ||
27 | spec = 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 @@ | |||
1 | Prerequisites | ||
2 | ============= | ||
3 | |||
4 | To 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! | ||
2 | module System.Torrent.FileMapSpec (spec) where | ||
3 | |||
4 | import Control.Monad.Loops | ||
5 | import Data.List as L | ||
6 | import Data.ByteString.Lazy as BL | ||
7 | import System.Directory | ||
8 | import System.FilePath | ||
9 | import System.IO.Temp | ||
10 | import Test.Hspec | ||
11 | |||
12 | import Data.Torrent | ||
13 | import System.Torrent.FileMap as FM | ||
14 | |||
15 | |||
16 | withLayout :: (FileLayout FileSize -> IO ()) -> IO () | ||
17 | withLayout 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 | |||
25 | spec :: Spec | ||
26 | spec = 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 @@ | |||
1 | module System.Torrent.StorageSpec (spec) where | ||
2 | import Data.ByteString.Lazy as BL | ||
3 | import Data.Conduit as C | ||
4 | import Data.Conduit.List as C | ||
5 | import System.FilePath | ||
6 | import System.Directory | ||
7 | import System.IO.Unsafe | ||
8 | import Test.Hspec | ||
9 | |||
10 | import Data.Torrent | ||
11 | import Network.BitTorrent.Exchange.Bitfield as BF | ||
12 | import System.Torrent.Storage | ||
13 | |||
14 | |||
15 | layout :: FileLayout FileSize | ||
16 | layout = | ||
17 | [ (dir </> "_a", 20) | ||
18 | , (dir </> "_b", 50) | ||
19 | , (dir </> "_c", 100) | ||
20 | , (dir </> "_d", 5) | ||
21 | ] | ||
22 | where | ||
23 | dir = unsafePerformIO $ getTemporaryDirectory | ||
24 | |||
25 | createLayout :: IO () | ||
26 | createLayout = withStorage ReadWriteEx 1 layout (const (return ())) | ||
27 | |||
28 | psize :: PieceSize | ||
29 | psize = 16 | ||
30 | |||
31 | pcount :: PieceCount | ||
32 | pcount = 11 | ||
33 | |||
34 | spec :: Spec | ||
35 | spec = 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 | ||
@@ -0,0 +1,8 @@ | |||
1 | #!/bin/sh | ||
2 | compile=ghc | ||
3 | defs="-DBENCODE_AESON -DTHREAD_DEBUG" | ||
4 | hide="-hide-package crypto-random -hide-package crypto-api -hide-package crypto-numbers -hide-package cryptohash -hide-package prettyclass" | ||
5 | cbits="cbits/*.c" | ||
6 | # -Wno-typed-holes | ||
7 | includes="-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 | ||
33 | static 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 | ||
41 | static 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 | ||
55 | static inline uint32_t bitfn_swap32(uint32_t a) | ||
56 | { | ||
57 | asm ("bswap %0" : "=r" (a) : "0" (a)); | ||
58 | return a; | ||
59 | } | ||
60 | |||
61 | static 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 | ||
72 | static 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 | ||
79 | static 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 | ||
86 | static 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 | ||
93 | static 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 | ||
100 | static 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 | ||
107 | static 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 | ||
115 | static 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 | ||
123 | static 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 | ||
131 | static 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 | ||
148 | static 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 | ||
155 | static 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 | ||
162 | static 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 | |||
37 | static const uint8_t sigma[16] = "expand 32-byte k"; | ||
38 | static 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 | |||
61 | static inline uint32_t load32(const uint8_t *p) | ||
62 | { | ||
63 | return le32_to_cpu(*((uint32_t *) p)); | ||
64 | } | ||
65 | |||
66 | static 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 | |||
101 | void 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) */ | ||
122 | void 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 | |||
162 | void 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 | |||
171 | void 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 | |||
230 | void 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 | |||
33 | typedef union { | ||
34 | uint64_t q[8]; | ||
35 | uint32_t d[16]; | ||
36 | uint8_t b[64]; | ||
37 | } block; | ||
38 | |||
39 | typedef block cryptonite_salsa_state; | ||
40 | |||
41 | typedef 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 */ | ||
50 | void cryptonite_salsa_core_xor(int rounds, block *out, block *in); | ||
51 | |||
52 | void cryptonite_salsa_init_core(cryptonite_salsa_state *st, uint32_t keylen, const uint8_t *key, uint32_t ivlen, const uint8_t *iv); | ||
53 | void 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); | ||
54 | void cryptonite_salsa_combine(uint8_t *dst, cryptonite_salsa_context *st, const uint8_t *src, uint32_t bytes); | ||
55 | void 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 | |||
35 | static 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 */ | ||
41 | void 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 | |||
35 | void 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 | ||
@@ -0,0 +1,7 @@ | |||
1 | #!/bin/sh | ||
2 | compile=ghci | ||
3 | defs="-DBENCODE_AESON -DTHREAD_DEBUG" | ||
4 | hide="-hide-package crypto-random -hide-package crypto-api -hide-package crypto-numbers -hide-package cryptohash -hide-package prettyclass" | ||
5 | # cbits="cbits/*.c" | ||
6 | includes="-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 #-} | ||
10 | module Crypto.Cipher.Salsa | ||
11 | ( initialize | ||
12 | , combine | ||
13 | , generate | ||
14 | , State(..) | ||
15 | ) where | ||
16 | |||
17 | import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, ScrubbedBytes) | ||
18 | import qualified Crypto.Internal.ByteArray as B | ||
19 | import Crypto.Internal.Compat | ||
20 | import Crypto.Internal.Imports | ||
21 | import Foreign.Ptr | ||
22 | import Foreign.C.Types | ||
23 | |||
24 | -- | Salsa context | ||
25 | newtype 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. | ||
30 | initialize :: (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 | ||
35 | initialize 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. | ||
50 | combine :: ByteArray ba | ||
51 | => State -- ^ the current Salsa state | ||
52 | -> ba -- ^ the source to xor with the generator | ||
53 | -> (ba, State) | ||
54 | combine 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 | ||
64 | generate :: ByteArray ba | ||
65 | => State -- ^ the current Salsa state | ||
66 | -> Int -- ^ the length of data to generate | ||
67 | -> (ba, State) | ||
68 | generate 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 | |||
76 | foreign import ccall "cryptonite_salsa_init" | ||
77 | ccryptonite_salsa_init :: Ptr State -> Int -> Int -> Ptr Word8 -> Int -> Ptr Word8 -> IO () | ||
78 | |||
79 | foreign import ccall "cryptonite_salsa_combine" | ||
80 | ccryptonite_salsa_combine :: Ptr Word8 -> Ptr State -> Ptr Word8 -> CUInt -> IO () | ||
81 | |||
82 | foreign 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 #-} | ||
13 | module Crypto.Cipher.XSalsa | ||
14 | ( initialize | ||
15 | , combine | ||
16 | , generate | ||
17 | , State | ||
18 | ) where | ||
19 | |||
20 | import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, ScrubbedBytes) | ||
21 | import qualified Crypto.Internal.ByteArray as B | ||
22 | import Crypto.Internal.Compat | ||
23 | import Crypto.Internal.Imports | ||
24 | import Foreign.Ptr | ||
25 | import Foreign.Storable | ||
26 | import Foreign.C.Types | ||
27 | import Crypto.Cipher.Salsa hiding (initialize) | ||
28 | |||
29 | -- | Initialize a new XSalsa context with the number of rounds, | ||
30 | -- the key and the nonce associated. | ||
31 | initialize :: (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 | ||
36 | initialize 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 | |||
49 | foreign 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 #-} | ||
12 | module Crypto.ECC.Class | ||
13 | ( Curve_X25519(..) | ||
14 | , EllipticCurve(..) | ||
15 | , EllipticCurveDH(..) | ||
16 | , EllipticCurveArith(..) | ||
17 | , KeyPair(..) | ||
18 | , SharedSecret(..) | ||
19 | ) where | ||
20 | |||
21 | import qualified Crypto.ECC.Simple.Types as Simple | ||
22 | import qualified Crypto.ECC.Simple.Prim as Simple | ||
23 | import Crypto.Random | ||
24 | -- import Crypto.Error | ||
25 | import Crypto.Error.Types | ||
26 | -- import Crypto.Internal.Proxy | ||
27 | import Data.Typeable | ||
28 | import Crypto.Internal.ByteArray (ByteArray, ByteArrayAccess, ScrubbedBytes) | ||
29 | import qualified Crypto.Internal.ByteArray as B | ||
30 | import Crypto.Number.Serialize (i2ospOf_, os2ip) | ||
31 | import qualified Crypto.PubKey.Curve25519 as X25519 | ||
32 | import Data.ByteArray (convert) | ||
33 | |||
34 | -- | An elliptic curve key pair composed of the private part (a scalar), and | ||
35 | -- the associated point. | ||
36 | data KeyPair curve = KeyPair | ||
37 | { keypairGetPublic :: !(Point curve) | ||
38 | , keypairGetPrivate :: !(Scalar curve) | ||
39 | } | ||
40 | |||
41 | newtype SharedSecret = SharedSecret ScrubbedBytes | ||
42 | deriving (Eq, ByteArrayAccess) | ||
43 | |||
44 | class 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 | |||
67 | class 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 | |||
76 | class 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 | |||
86 | data Curve_X25519 = Curve_X25519 | ||
87 | |||
88 | instance 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 | |||
99 | instance EllipticCurveDH Curve_X25519 where | ||
100 | ecdh _ s p = SharedSecret $ convert secret | ||
101 | where secret = X25519.dh p s | ||
102 | |||
103 | encodeECPoint :: forall curve bs . (Simple.Curve curve, ByteArray bs) => Simple.Point curve -> bs | ||
104 | encodeECPoint Simple.PointO = error "encodeECPoint: cannot serialize point at infinity" | ||
105 | encodeECPoint (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 | |||
113 | decodeECPoint :: (Simple.Curve curve, ByteArray bs) => bs -> CryptoFailable (Simple.Point curve) | ||
114 | decodeECPoint 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 | |||
126 | curveSizeBytes :: EllipticCurve c => Proxy c -> Int | ||
127 | curveSizeBytes 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 #-} | ||
5 | module 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 | |||
18 | import Data.Maybe | ||
19 | import Data.Typeable | ||
20 | import Crypto.Internal.Imports | ||
21 | import Crypto.Number.ModArithmetic | ||
22 | import Crypto.Number.F2m | ||
23 | import Crypto.Number.Generate (generateBetween) | ||
24 | import Crypto.ECC.Simple.Types | ||
25 | -- import Crypto.Error | ||
26 | import Crypto.Error.Types | ||
27 | import Crypto.Random | ||
28 | |||
29 | -- | Generate a valid scalar for a specific Curve | ||
30 | scalarGenerate :: forall randomly curve . (MonadRandom randomly, Curve curve) => randomly (Scalar curve) | ||
31 | scalarGenerate = | ||
32 | Scalar <$> generateBetween 1 (n - 1) | ||
33 | where | ||
34 | n = curveEccN $ curveParameters (Proxy :: Proxy curve) | ||
35 | |||
36 | scalarFromInteger :: forall curve . Curve curve => Integer -> CryptoFailable (Scalar curve) | ||
37 | scalarFromInteger 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@. | ||
49 | pointNegate :: Curve curve => Point curve -> Point curve | ||
50 | pointNegate PointO = PointO | ||
51 | pointNegate 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. | ||
59 | pointAdd :: Curve curve => Point curve -> Point curve -> Point curve | ||
60 | pointAdd PointO PointO = PointO | ||
61 | pointAdd PointO q = q | ||
62 | pointAdd p PointO = p | ||
63 | pointAdd p q | ||
64 | | p == q = pointDouble p | ||
65 | | p == pointNegate q = PointO | ||
66 | pointAdd 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 | -- | ||
99 | pointDouble :: Curve curve => Point curve -> Point curve | ||
100 | pointDouble PointO = PointO | ||
101 | pointDouble 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. | ||
123 | pointBaseMul :: Curve curve => Scalar curve -> Point curve | ||
124 | pointBaseMul 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. | ||
129 | pointMul :: Curve curve => Scalar curve -> Point curve -> Point curve | ||
130 | pointMul _ PointO = PointO | ||
131 | pointMul (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. | ||
143 | pointAddTwoMuls :: Curve curve => Scalar curve -> Point curve -> Scalar curve -> Point curve -> Point curve | ||
144 | pointAddTwoMuls _ PointO _ PointO = PointO | ||
145 | pointAddTwoMuls _ PointO n2 p2 = pointMul n2 p2 | ||
146 | pointAddTwoMuls n1 p1 _ PointO = pointMul n1 p1 | ||
147 | pointAddTwoMuls (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. | ||
161 | isPointAtInfinity :: Point curve -> Bool | ||
162 | isPointAtInfinity PointO = True | ||
163 | isPointAtInfinity _ = 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 | ||
169 | pointFromIntegers :: forall curve . Curve curve => (Integer, Integer) -> CryptoFailable (Point curve) | ||
170 | pointFromIntegers (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 | ||
181 | isPointValid :: Curve curve => proxy curve -> Integer -> Integer -> Bool | ||
182 | isPointValid 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 | ||
205 | divmod :: Integer -> Integer -> Integer -> Maybe Integer | ||
206 | divmod 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 #-} | ||
13 | module 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 | |||
59 | import Data.Data | ||
60 | import Crypto.Internal.Imports | ||
61 | import Crypto.Number.Basic (numBits) | ||
62 | |||
63 | class Curve curve where | ||
64 | curveParameters :: proxy curve -> CurveParameters curve | ||
65 | curveType :: proxy curve -> CurveType | ||
66 | |||
67 | -- | get the size of the curve in bits | ||
68 | curveSizeBits :: Curve curve => proxy curve -> Int | ||
69 | curveSizeBits 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 | ||
75 | curveSizeBytes :: Curve curve => proxy curve -> Int | ||
76 | curveSizeBytes 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. | ||
80 | data 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 | |||
88 | newtype CurveBinaryParam = CurveBinaryParam Integer | ||
89 | deriving (Show,Read,Eq,Data,Typeable) | ||
90 | |||
91 | newtype CurvePrimeParam = CurvePrimeParam Integer | ||
92 | deriving (Show,Read,Eq,Data,Typeable) | ||
93 | |||
94 | data CurveType = | ||
95 | CurveBinary CurveBinaryParam | ||
96 | | CurvePrime CurvePrimeParam | ||
97 | deriving (Show,Read,Eq,Data,Typeable) | ||
98 | |||
99 | -- | ECC Private Number | ||
100 | newtype Scalar curve = Scalar Integer | ||
101 | deriving (Show,Read,Eq,Data,Typeable) | ||
102 | |||
103 | -- | Define a point on a curve. | ||
104 | data Point curve = | ||
105 | Point Integer Integer | ||
106 | | PointO -- ^ Point at Infinity | ||
107 | deriving (Show,Read,Eq,Data,Typeable) | ||
108 | |||
109 | instance NFData (Point curve) where | ||
110 | rnf (Point x y) = x `seq` y `seq` () | ||
111 | rnf PointO = () | ||
112 | |||
113 | data SEC_p112r1 = SEC_p112r1 deriving (Show,Read,Eq) | ||
114 | data SEC_p112r2 = SEC_p112r2 deriving (Show,Read,Eq) | ||
115 | data SEC_p128r1 = SEC_p128r1 deriving (Show,Read,Eq) | ||
116 | data SEC_p128r2 = SEC_p128r2 deriving (Show,Read,Eq) | ||
117 | data SEC_p160k1 = SEC_p160k1 deriving (Show,Read,Eq) | ||
118 | data SEC_p160r1 = SEC_p160r1 deriving (Show,Read,Eq) | ||
119 | data SEC_p160r2 = SEC_p160r2 deriving (Show,Read,Eq) | ||
120 | data SEC_p192k1 = SEC_p192k1 deriving (Show,Read,Eq) | ||
121 | data SEC_p192r1 = SEC_p192r1 deriving (Show,Read,Eq) | ||
122 | data SEC_p224k1 = SEC_p224k1 deriving (Show,Read,Eq) | ||
123 | data SEC_p224r1 = SEC_p224r1 deriving (Show,Read,Eq) | ||
124 | data SEC_p256k1 = SEC_p256k1 deriving (Show,Read,Eq) | ||
125 | data SEC_p256r1 = SEC_p256r1 deriving (Show,Read,Eq) | ||
126 | data SEC_p384r1 = SEC_p384r1 deriving (Show,Read,Eq) | ||
127 | data SEC_p521r1 = SEC_p521r1 deriving (Show,Read,Eq) | ||
128 | data SEC_t113r1 = SEC_t113r1 deriving (Show,Read,Eq) | ||
129 | data SEC_t113r2 = SEC_t113r2 deriving (Show,Read,Eq) | ||
130 | data SEC_t131r1 = SEC_t131r1 deriving (Show,Read,Eq) | ||
131 | data SEC_t131r2 = SEC_t131r2 deriving (Show,Read,Eq) | ||
132 | data SEC_t163k1 = SEC_t163k1 deriving (Show,Read,Eq) | ||
133 | data SEC_t163r1 = SEC_t163r1 deriving (Show,Read,Eq) | ||
134 | data SEC_t163r2 = SEC_t163r2 deriving (Show,Read,Eq) | ||
135 | data SEC_t193r1 = SEC_t193r1 deriving (Show,Read,Eq) | ||
136 | data SEC_t193r2 = SEC_t193r2 deriving (Show,Read,Eq) | ||
137 | data SEC_t233k1 = SEC_t233k1 deriving (Show,Read,Eq) | ||
138 | data SEC_t233r1 = SEC_t233r1 deriving (Show,Read,Eq) | ||
139 | data SEC_t239k1 = SEC_t239k1 deriving (Show,Read,Eq) | ||
140 | data SEC_t283k1 = SEC_t283k1 deriving (Show,Read,Eq) | ||
141 | data SEC_t283r1 = SEC_t283r1 deriving (Show,Read,Eq) | ||
142 | data SEC_t409k1 = SEC_t409k1 deriving (Show,Read,Eq) | ||
143 | data SEC_t409r1 = SEC_t409r1 deriving (Show,Read,Eq) | ||
144 | data SEC_t571k1 = SEC_t571k1 deriving (Show,Read,Eq) | ||
145 | data SEC_t571r1 = SEC_t571r1 deriving (Show,Read,Eq) | ||
146 | |||
147 | -- | Define names for known recommended curves. | ||
148 | instance Curve SEC_p112r1 where | ||
149 | curveType _ = typeSEC_p112r1 | ||
150 | curveParameters _ = paramSEC_p112r1 | ||
151 | |||
152 | instance Curve SEC_p112r2 where | ||
153 | curveType _ = typeSEC_p112r2 | ||
154 | curveParameters _ = paramSEC_p112r2 | ||
155 | |||
156 | instance Curve SEC_p128r1 where | ||
157 | curveType _ = typeSEC_p128r1 | ||
158 | curveParameters _ = paramSEC_p128r1 | ||
159 | |||
160 | instance Curve SEC_p128r2 where | ||
161 | curveType _ = typeSEC_p128r2 | ||
162 | curveParameters _ = paramSEC_p128r2 | ||
163 | |||
164 | instance Curve SEC_p160k1 where | ||
165 | curveType _ = typeSEC_p160k1 | ||
166 | curveParameters _ = paramSEC_p160k1 | ||
167 | |||
168 | instance Curve SEC_p160r1 where | ||
169 | curveType _ = typeSEC_p160r1 | ||
170 | curveParameters _ = paramSEC_p160r1 | ||
171 | |||
172 | instance Curve SEC_p160r2 where | ||
173 | curveType _ = typeSEC_p160r2 | ||
174 | curveParameters _ = paramSEC_p160r2 | ||
175 | |||
176 | instance Curve SEC_p192k1 where | ||
177 | curveType _ = typeSEC_p192k1 | ||
178 | curveParameters _ = paramSEC_p192k1 | ||
179 | |||
180 | instance Curve SEC_p192r1 where | ||
181 | curveType _ = typeSEC_p192r1 | ||
182 | curveParameters _ = paramSEC_p192r1 | ||
183 | |||
184 | instance Curve SEC_p224k1 where | ||
185 | curveType _ = typeSEC_p224k1 | ||
186 | curveParameters _ = paramSEC_p224k1 | ||
187 | |||
188 | instance Curve SEC_p224r1 where | ||
189 | curveType _ = typeSEC_p224r1 | ||
190 | curveParameters _ = paramSEC_p224r1 | ||
191 | |||
192 | instance Curve SEC_p256k1 where | ||
193 | curveType _ = typeSEC_p256k1 | ||
194 | curveParameters _ = paramSEC_p256k1 | ||
195 | |||
196 | instance Curve SEC_p256r1 where | ||
197 | curveType _ = typeSEC_p256r1 | ||
198 | curveParameters _ = paramSEC_p256r1 | ||
199 | |||
200 | instance Curve SEC_p384r1 where | ||
201 | curveType _ = typeSEC_p384r1 | ||
202 | curveParameters _ = paramSEC_p384r1 | ||
203 | |||
204 | instance Curve SEC_p521r1 where | ||
205 | curveType _ = typeSEC_p521r1 | ||
206 | curveParameters _ = paramSEC_p521r1 | ||
207 | |||
208 | instance Curve SEC_t113r1 where | ||
209 | curveType _ = typeSEC_t113r1 | ||
210 | curveParameters _ = paramSEC_t113r1 | ||
211 | |||
212 | instance Curve SEC_t113r2 where | ||
213 | curveType _ = typeSEC_t113r2 | ||
214 | curveParameters _ = paramSEC_t113r2 | ||
215 | |||
216 | instance Curve SEC_t131r1 where | ||
217 | curveType _ = typeSEC_t131r1 | ||
218 | curveParameters _ = paramSEC_t131r1 | ||
219 | |||
220 | instance Curve SEC_t131r2 where | ||
221 | curveType _ = typeSEC_t131r2 | ||
222 | curveParameters _ = paramSEC_t131r2 | ||
223 | |||
224 | instance Curve SEC_t163k1 where | ||
225 | curveType _ = typeSEC_t163k1 | ||
226 | curveParameters _ = paramSEC_t163k1 | ||
227 | |||
228 | instance Curve SEC_t163r1 where | ||
229 | curveType _ = typeSEC_t163r1 | ||
230 | curveParameters _ = paramSEC_t163r1 | ||
231 | |||
232 | instance Curve SEC_t163r2 where | ||
233 | curveType _ = typeSEC_t163r2 | ||
234 | curveParameters _ = paramSEC_t163r2 | ||
235 | |||
236 | instance Curve SEC_t193r1 where | ||
237 | curveType _ = typeSEC_t193r1 | ||
238 | curveParameters _ = paramSEC_t193r1 | ||
239 | |||
240 | instance Curve SEC_t193r2 where | ||
241 | curveType _ = typeSEC_t193r2 | ||
242 | curveParameters _ = paramSEC_t193r2 | ||
243 | |||
244 | instance Curve SEC_t233k1 where | ||
245 | curveType _ = typeSEC_t233k1 | ||
246 | curveParameters _ = paramSEC_t233k1 | ||
247 | |||
248 | instance Curve SEC_t233r1 where | ||
249 | curveType _ = typeSEC_t233r1 | ||
250 | curveParameters _ = paramSEC_t233r1 | ||
251 | |||
252 | instance Curve SEC_t239k1 where | ||
253 | curveType _ = typeSEC_t239k1 | ||
254 | curveParameters _ = paramSEC_t239k1 | ||
255 | |||
256 | instance Curve SEC_t283k1 where | ||
257 | curveType _ = typeSEC_t283k1 | ||
258 | curveParameters _ = paramSEC_t283k1 | ||
259 | |||
260 | instance Curve SEC_t283r1 where | ||
261 | curveType _ = typeSEC_t283r1 | ||
262 | curveParameters _ = paramSEC_t283r1 | ||
263 | |||
264 | instance Curve SEC_t409k1 where | ||
265 | curveType _ = typeSEC_t409k1 | ||
266 | curveParameters _ = paramSEC_t409k1 | ||
267 | |||
268 | instance Curve SEC_t409r1 where | ||
269 | curveType _ = typeSEC_t409r1 | ||
270 | curveParameters _ = paramSEC_t409r1 | ||
271 | |||
272 | instance Curve SEC_t571k1 where | ||
273 | curveType _ = typeSEC_t571k1 | ||
274 | curveParameters _ = paramSEC_t571k1 | ||
275 | |||
276 | instance Curve SEC_t571r1 where | ||
277 | curveType _ = typeSEC_t571r1 | ||
278 | curveParameters _ = paramSEC_t571r1 | ||
279 | |||
280 | {- | ||
281 | curvesOIDs :: [ (CurveName, [Integer]) ] | ||
282 | curvesOIDs = | ||
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 | |||
319 | typeSEC_p112r1 = CurvePrime $ CurvePrimeParam 0xdb7c2abf62e35e668076bead208b | ||
320 | paramSEC_p112r1 = CurveParameters | ||
321 | { curveEccA = 0xdb7c2abf62e35e668076bead2088 | ||
322 | , curveEccB = 0x659ef8ba043916eede8911702b22 | ||
323 | , curveEccG = Point 0x09487239995a5ee76b55f9c2f098 | ||
324 | 0xa89ce5af8724c0a23e0e0ff77500 | ||
325 | , curveEccN = 0xdb7c2abf62e35e7628dfac6561c5 | ||
326 | , curveEccH = 1 | ||
327 | } | ||
328 | typeSEC_p112r2 = CurvePrime $ CurvePrimeParam 0xdb7c2abf62e35e668076bead208b | ||
329 | paramSEC_p112r2 = CurveParameters | ||
330 | { curveEccA = 0x6127c24c05f38a0aaaf65c0ef02c | ||
331 | , curveEccB = 0x51def1815db5ed74fcc34c85d709 | ||
332 | , curveEccG = Point 0x4ba30ab5e892b4e1649dd0928643 | ||
333 | 0xadcd46f5882e3747def36e956e97 | ||
334 | , curveEccN = 0x36df0aafd8b8d7597ca10520d04b | ||
335 | , curveEccH = 4 | ||
336 | } | ||
337 | typeSEC_p128r1 = CurvePrime $ CurvePrimeParam 0xfffffffdffffffffffffffffffffffff | ||
338 | paramSEC_p128r1 = CurveParameters | ||
339 | { curveEccA = 0xfffffffdfffffffffffffffffffffffc | ||
340 | , curveEccB = 0xe87579c11079f43dd824993c2cee5ed3 | ||
341 | , curveEccG = Point 0x161ff7528b899b2d0c28607ca52c5b86 | ||
342 | 0xcf5ac8395bafeb13c02da292dded7a83 | ||
343 | , curveEccN = 0xfffffffe0000000075a30d1b9038a115 | ||
344 | , curveEccH = 1 | ||
345 | } | ||
346 | typeSEC_p128r2 = CurvePrime $ CurvePrimeParam 0xfffffffdffffffffffffffffffffffff | ||
347 | paramSEC_p128r2 = CurveParameters | ||
348 | { curveEccA = 0xd6031998d1b3bbfebf59cc9bbff9aee1 | ||
349 | , curveEccB = 0x5eeefca380d02919dc2c6558bb6d8a5d | ||
350 | , curveEccG = Point 0x7b6aa5d85e572983e6fb32a7cdebc140 | ||
351 | 0x27b6916a894d3aee7106fe805fc34b44 | ||
352 | , curveEccN = 0x3fffffff7fffffffbe0024720613b5a3 | ||
353 | , curveEccH = 4 | ||
354 | } | ||
355 | typeSEC_p160k1 = CurvePrime $ CurvePrimeParam 0x00fffffffffffffffffffffffffffffffeffffac73 | ||
356 | paramSEC_p160k1 = CurveParameters | ||
357 | { curveEccA = 0x000000000000000000000000000000000000000000 | ||
358 | , curveEccB = 0x000000000000000000000000000000000000000007 | ||
359 | , curveEccG = Point 0x003b4c382ce37aa192a4019e763036f4f5dd4d7ebb | ||
360 | 0x00938cf935318fdced6bc28286531733c3f03c4fee | ||
361 | , curveEccN = 0x0100000000000000000001b8fa16dfab9aca16b6b3 | ||
362 | , curveEccH = 1 | ||
363 | } | ||
364 | typeSEC_p160r1 = CurvePrime $ CurvePrimeParam 0x00ffffffffffffffffffffffffffffffff7fffffff | ||
365 | paramSEC_p160r1 = CurveParameters | ||
366 | { curveEccA = 0x00ffffffffffffffffffffffffffffffff7ffffffc | ||
367 | , curveEccB = 0x001c97befc54bd7a8b65acf89f81d4d4adc565fa45 | ||
368 | , curveEccG = Point 0x004a96b5688ef573284664698968c38bb913cbfc82 | ||
369 | 0x0023a628553168947d59dcc912042351377ac5fb32 | ||
370 | , curveEccN = 0x0100000000000000000001f4c8f927aed3ca752257 | ||
371 | , curveEccH = 1 | ||
372 | } | ||
373 | typeSEC_p160r2 = CurvePrime $ CurvePrimeParam 0x00fffffffffffffffffffffffffffffffeffffac73 | ||
374 | paramSEC_p160r2 = CurveParameters | ||
375 | { curveEccA = 0x00fffffffffffffffffffffffffffffffeffffac70 | ||
376 | , curveEccB = 0x00b4e134d3fb59eb8bab57274904664d5af50388ba | ||
377 | , curveEccG = Point 0x0052dcb034293a117e1f4ff11b30f7199d3144ce6d | ||
378 | 0x00feaffef2e331f296e071fa0df9982cfea7d43f2e | ||
379 | , curveEccN = 0x0100000000000000000000351ee786a818f3a1a16b | ||
380 | , curveEccH = 1 | ||
381 | } | ||
382 | typeSEC_p192k1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffffffffffeffffee37 | ||
383 | paramSEC_p192k1 = CurveParameters | ||
384 | { curveEccA = 0x000000000000000000000000000000000000000000000000 | ||
385 | , curveEccB = 0x000000000000000000000000000000000000000000000003 | ||
386 | , curveEccG = Point 0xdb4ff10ec057e9ae26b07d0280b7f4341da5d1b1eae06c7d | ||
387 | 0x9b2f2f6d9c5628a7844163d015be86344082aa88d95e2f9d | ||
388 | , curveEccN = 0xfffffffffffffffffffffffe26f2fc170f69466a74defd8d | ||
389 | , curveEccH = 1 | ||
390 | } | ||
391 | typeSEC_p192r1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffeffffffffffffffff | ||
392 | paramSEC_p192r1 = CurveParameters | ||
393 | { curveEccA = 0xfffffffffffffffffffffffffffffffefffffffffffffffc | ||
394 | , curveEccB = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1 | ||
395 | , curveEccG = Point 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012 | ||
396 | 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811 | ||
397 | , curveEccN = 0xffffffffffffffffffffffff99def836146bc9b1b4d22831 | ||
398 | , curveEccH = 1 | ||
399 | } | ||
400 | typeSEC_p224k1 = CurvePrime $ CurvePrimeParam 0x00fffffffffffffffffffffffffffffffffffffffffffffffeffffe56d | ||
401 | paramSEC_p224k1 = CurveParameters | ||
402 | { curveEccA = 0x0000000000000000000000000000000000000000000000000000000000 | ||
403 | , curveEccB = 0x0000000000000000000000000000000000000000000000000000000005 | ||
404 | , curveEccG = Point 0x00a1455b334df099df30fc28a169a467e9e47075a90f7e650eb6b7a45c | ||
405 | 0x007e089fed7fba344282cafbd6f7e319f7c0b0bd59e2ca4bdb556d61a5 | ||
406 | , curveEccN = 0x010000000000000000000000000001dce8d2ec6184caf0a971769fb1f7 | ||
407 | , curveEccH = 1 | ||
408 | } | ||
409 | typeSEC_p224r1 = CurvePrime $ CurvePrimeParam 0xffffffffffffffffffffffffffffffff000000000000000000000001 | ||
410 | paramSEC_p224r1 = CurveParameters | ||
411 | { curveEccA = 0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe | ||
412 | , curveEccB = 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4 | ||
413 | , curveEccG = Point 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21 | ||
414 | 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34 | ||
415 | , curveEccN = 0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d | ||
416 | , curveEccH = 1 | ||
417 | } | ||
418 | typeSEC_p256k1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f | ||
419 | paramSEC_p256k1 = CurveParameters | ||
420 | { curveEccA = 0x0000000000000000000000000000000000000000000000000000000000000000 | ||
421 | , curveEccB = 0x0000000000000000000000000000000000000000000000000000000000000007 | ||
422 | , curveEccG = Point 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 | ||
423 | 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 | ||
424 | , curveEccN = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 | ||
425 | , curveEccH = 1 | ||
426 | } | ||
427 | typeSEC_p256r1 = CurvePrime $ CurvePrimeParam 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff | ||
428 | paramSEC_p256r1 = CurveParameters | ||
429 | { curveEccA = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc | ||
430 | , curveEccB = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b | ||
431 | , curveEccG = Point 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296 | ||
432 | 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5 | ||
433 | , curveEccN = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 | ||
434 | , curveEccH = 1 | ||
435 | } | ||
436 | typeSEC_p384r1 = CurvePrime $ CurvePrimeParam 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff | ||
437 | paramSEC_p384r1 = CurveParameters | ||
438 | { curveEccA = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc | ||
439 | , curveEccB = 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef | ||
440 | , curveEccG = Point 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7 | ||
441 | 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f | ||
442 | , curveEccN = 0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973 | ||
443 | , curveEccH = 1 | ||
444 | } | ||
445 | typeSEC_p521r1 = CurvePrime $ CurvePrimeParam 0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff | ||
446 | paramSEC_p521r1 = CurveParameters | ||
447 | { curveEccA = 0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc | ||
448 | , curveEccB = 0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 | ||
449 | , curveEccG = Point 0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66 | ||
450 | 0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650 | ||
451 | , curveEccN = 0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409 | ||
452 | , curveEccH = 1 | ||
453 | } | ||
454 | typeSEC_t113r1 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000201 | ||
455 | paramSEC_t113r1 = CurveParameters | ||
456 | { curveEccA = 0x003088250ca6e7c7fe649ce85820f7 | ||
457 | , curveEccB = 0x00e8bee4d3e2260744188be0e9c723 | ||
458 | , curveEccG = Point 0x009d73616f35f4ab1407d73562c10f | ||
459 | 0x00a52830277958ee84d1315ed31886 | ||
460 | , curveEccN = 0x0100000000000000d9ccec8a39e56f | ||
461 | , curveEccH = 2 | ||
462 | } | ||
463 | typeSEC_t113r2 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000201 | ||
464 | paramSEC_t113r2 = CurveParameters | ||
465 | { curveEccA = 0x00689918dbec7e5a0dd6dfc0aa55c7 | ||
466 | , curveEccB = 0x0095e9a9ec9b297bd4bf36e059184f | ||
467 | , curveEccG = Point 0x01a57a6a7b26ca5ef52fcdb8164797 | ||
468 | 0x00b3adc94ed1fe674c06e695baba1d | ||
469 | , curveEccN = 0x010000000000000108789b2496af93 | ||
470 | , curveEccH = 2 | ||
471 | } | ||
472 | typeSEC_t131r1 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000010d | ||
473 | paramSEC_t131r1 = CurveParameters | ||
474 | { curveEccA = 0x07a11b09a76b562144418ff3ff8c2570b8 | ||
475 | , curveEccB = 0x0217c05610884b63b9c6c7291678f9d341 | ||
476 | , curveEccG = Point 0x0081baf91fdf9833c40f9c181343638399 | ||
477 | 0x078c6e7ea38c001f73c8134b1b4ef9e150 | ||
478 | , curveEccN = 0x0400000000000000023123953a9464b54d | ||
479 | , curveEccH = 2 | ||
480 | } | ||
481 | typeSEC_t131r2 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000010d | ||
482 | paramSEC_t131r2 = CurveParameters | ||
483 | { curveEccA = 0x03e5a88919d7cafcbf415f07c2176573b2 | ||
484 | , curveEccB = 0x04b8266a46c55657ac734ce38f018f2192 | ||
485 | , curveEccG = Point 0x0356dcd8f2f95031ad652d23951bb366a8 | ||
486 | 0x0648f06d867940a5366d9e265de9eb240f | ||
487 | , curveEccN = 0x0400000000000000016954a233049ba98f | ||
488 | , curveEccH = 2 | ||
489 | } | ||
490 | typeSEC_t163k1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000c9 | ||
491 | paramSEC_t163k1 = CurveParameters | ||
492 | { curveEccA = 0x000000000000000000000000000000000000000001 | ||
493 | , curveEccB = 0x000000000000000000000000000000000000000001 | ||
494 | , curveEccG = Point 0x02fe13c0537bbc11acaa07d793de4e6d5e5c94eee8 | ||
495 | 0x0289070fb05d38ff58321f2e800536d538ccdaa3d9 | ||
496 | , curveEccN = 0x04000000000000000000020108a2e0cc0d99f8a5ef | ||
497 | , curveEccH = 2 | ||
498 | } | ||
499 | typeSEC_t163r1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000c9 | ||
500 | paramSEC_t163r1 = CurveParameters | ||
501 | { curveEccA = 0x07b6882caaefa84f9554ff8428bd88e246d2782ae2 | ||
502 | , curveEccB = 0x0713612dcddcb40aab946bda29ca91f73af958afd9 | ||
503 | , curveEccG = Point 0x0369979697ab43897789566789567f787a7876a654 | ||
504 | 0x00435edb42efafb2989d51fefce3c80988f41ff883 | ||
505 | , curveEccN = 0x03ffffffffffffffffffff48aab689c29ca710279b | ||
506 | , curveEccH = 2 | ||
507 | } | ||
508 | typeSEC_t163r2 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000c9 | ||
509 | paramSEC_t163r2 = CurveParameters | ||
510 | { curveEccA = 0x000000000000000000000000000000000000000001 | ||
511 | , curveEccB = 0x020a601907b8c953ca1481eb10512f78744a3205fd | ||
512 | , curveEccG = Point 0x03f0eba16286a2d57ea0991168d4994637e8343e36 | ||
513 | 0x00d51fbc6c71a0094fa2cdd545b11c5c0c797324f1 | ||
514 | , curveEccN = 0x040000000000000000000292fe77e70c12a4234c33 | ||
515 | , curveEccH = 2 | ||
516 | } | ||
517 | typeSEC_t193r1 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000008001 | ||
518 | paramSEC_t193r1 = CurveParameters | ||
519 | { curveEccA = 0x0017858feb7a98975169e171f77b4087de098ac8a911df7b01 | ||
520 | , curveEccB = 0x00fdfb49bfe6c3a89facadaa7a1e5bbc7cc1c2e5d831478814 | ||
521 | , curveEccG = Point 0x01f481bc5f0ff84a74ad6cdf6fdef4bf6179625372d8c0c5e1 | ||
522 | 0x0025e399f2903712ccf3ea9e3a1ad17fb0b3201b6af7ce1b05 | ||
523 | , curveEccN = 0x01000000000000000000000000c7f34a778f443acc920eba49 | ||
524 | , curveEccH = 2 | ||
525 | } | ||
526 | typeSEC_t193r2 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000008001 | ||
527 | paramSEC_t193r2 = CurveParameters | ||
528 | { curveEccA = 0x0163f35a5137c2ce3ea6ed8667190b0bc43ecd69977702709b | ||
529 | , curveEccB = 0x00c9bb9e8927d4d64c377e2ab2856a5b16e3efb7f61d4316ae | ||
530 | , curveEccG = Point 0x00d9b67d192e0367c803f39e1a7e82ca14a651350aae617e8f | ||
531 | 0x01ce94335607c304ac29e7defbd9ca01f596f927224cdecf6c | ||
532 | , curveEccN = 0x010000000000000000000000015aab561b005413ccd4ee99d5 | ||
533 | , curveEccH = 2 | ||
534 | } | ||
535 | typeSEC_t233k1 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000000000000000004000000000000000001 | ||
536 | paramSEC_t233k1 = CurveParameters | ||
537 | { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000 | ||
538 | , curveEccB = 0x000000000000000000000000000000000000000000000000000000000001 | ||
539 | , curveEccG = Point 0x017232ba853a7e731af129f22ff4149563a419c26bf50a4c9d6eefad6126 | ||
540 | 0x01db537dece819b7f70f555a67c427a8cd9bf18aeb9b56e0c11056fae6a3 | ||
541 | , curveEccN = 0x008000000000000000000000000000069d5bb915bcd46efb1ad5f173abdf | ||
542 | , curveEccH = 4 | ||
543 | } | ||
544 | typeSEC_t233r1 = CurveBinary $ CurveBinaryParam 0x020000000000000000000000000000000000000004000000000000000001 | ||
545 | paramSEC_t233r1 = CurveParameters | ||
546 | { curveEccA = 0x000000000000000000000000000000000000000000000000000000000001 | ||
547 | , curveEccB = 0x0066647ede6c332c7f8c0923bb58213b333b20e9ce4281fe115f7d8f90ad | ||
548 | , curveEccG = Point 0x00fac9dfcbac8313bb2139f1bb755fef65bc391f8b36f8f8eb7371fd558b | ||
549 | 0x01006a08a41903350678e58528bebf8a0beff867a7ca36716f7e01f81052 | ||
550 | , curveEccN = 0x01000000000000000000000000000013e974e72f8a6922031d2603cfe0d7 | ||
551 | , curveEccH = 2 | ||
552 | } | ||
553 | typeSEC_t239k1 = CurveBinary $ CurveBinaryParam 0x800000000000000000004000000000000000000000000000000000000001 | ||
554 | paramSEC_t239k1 = CurveParameters | ||
555 | { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000 | ||
556 | , curveEccB = 0x000000000000000000000000000000000000000000000000000000000001 | ||
557 | , curveEccG = Point 0x29a0b6a887a983e9730988a68727a8b2d126c44cc2cc7b2a6555193035dc | ||
558 | 0x76310804f12e549bdb011c103089e73510acb275fc312a5dc6b76553f0ca | ||
559 | , curveEccN = 0x2000000000000000000000000000005a79fec67cb6e91f1c1da800e478a5 | ||
560 | , curveEccH = 4 | ||
561 | } | ||
562 | typeSEC_t283k1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000000000000000000000000000000010a1 | ||
563 | paramSEC_t283k1 = CurveParameters | ||
564 | { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000000 | ||
565 | , curveEccB = 0x000000000000000000000000000000000000000000000000000000000000000000000001 | ||
566 | , curveEccG = Point 0x0503213f78ca44883f1a3b8162f188e553cd265f23c1567a16876913b0c2ac2458492836 | ||
567 | 0x01ccda380f1c9e318d90f95d07e5426fe87e45c0e8184698e45962364e34116177dd2259 | ||
568 | , curveEccN = 0x01ffffffffffffffffffffffffffffffffffe9ae2ed07577265dff7f94451e061e163c61 | ||
569 | , curveEccH = 4 | ||
570 | } | ||
571 | typeSEC_t283r1 = CurveBinary $ CurveBinaryParam 0x0800000000000000000000000000000000000000000000000000000000000000000010a1 | ||
572 | paramSEC_t283r1 = CurveParameters | ||
573 | { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000001 | ||
574 | , curveEccB = 0x027b680ac8b8596da5a4af8a19a0303fca97fd7645309fa2a581485af6263e313b79a2f5 | ||
575 | , curveEccG = Point 0x05f939258db7dd90e1934f8c70b0dfec2eed25b8557eac9c80e2e198f8cdbecd86b12053 | ||
576 | 0x03676854fe24141cb98fe6d4b20d02b4516ff702350eddb0826779c813f0df45be8112f4 | ||
577 | , curveEccN = 0x03ffffffffffffffffffffffffffffffffffef90399660fc938a90165b042a7cefadb307 | ||
578 | , curveEccH = 2 | ||
579 | } | ||
580 | typeSEC_t409k1 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000001 | ||
581 | paramSEC_t409k1 = CurveParameters | ||
582 | { curveEccA = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 | ||
583 | , curveEccB = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 | ||
584 | , curveEccG = Point 0x0060f05f658f49c1ad3ab1890f7184210efd0987e307c84c27accfb8f9f67cc2c460189eb5aaaa62ee222eb1b35540cfe9023746 | ||
585 | 0x01e369050b7c4e42acba1dacbf04299c3460782f918ea427e6325165e9ea10e3da5f6c42e9c55215aa9ca27a5863ec48d8e0286b | ||
586 | , curveEccN = 0x007ffffffffffffffffffffffffffffffffffffffffffffffffffe5f83b2d4ea20400ec4557d5ed3e3e7ca5b4b5c83b8e01e5fcf | ||
587 | , curveEccH = 4 | ||
588 | } | ||
589 | typeSEC_t409r1 = CurveBinary $ CurveBinaryParam 0x02000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000001 | ||
590 | paramSEC_t409r1 = CurveParameters | ||
591 | { curveEccA = 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 | ||
592 | , curveEccB = 0x0021a5c2c8ee9feb5c4b9a753b7b476b7fd6422ef1f3dd674761fa99d6ac27c8a9a197b272822f6cd57a55aa4f50ae317b13545f | ||
593 | , curveEccG = Point 0x015d4860d088ddb3496b0c6064756260441cde4af1771d4db01ffe5b34e59703dc255a868a1180515603aeab60794e54bb7996a7 | ||
594 | 0x0061b1cfab6be5f32bbfa78324ed106a7636b9c5a7bd198d0158aa4f5488d08f38514f1fdf4b4f40d2181b3681c364ba0273c706 | ||
595 | , curveEccN = 0x010000000000000000000000000000000000000000000000000001e2aad6a612f33307be5fa47c3c9e052f838164cd37d9a21173 | ||
596 | , curveEccH = 2 | ||
597 | } | ||
598 | typeSEC_t571k1 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000425 | ||
599 | paramSEC_t571k1 = CurveParameters | ||
600 | { curveEccA = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 | ||
601 | , curveEccB = 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 | ||
602 | , curveEccG = Point 0x026eb7a859923fbc82189631f8103fe4ac9ca2970012d5d46024804801841ca44370958493b205e647da304db4ceb08cbbd1ba39494776fb988b47174dca88c7e2945283a01c8972 | ||
603 | 0x0349dc807f4fbf374f4aeade3bca95314dd58cec9f307a54ffc61efc006d8a2c9d4979c0ac44aea74fbebbb9f772aedcb620b01a7ba7af1b320430c8591984f601cd4c143ef1c7a3 | ||
604 | , curveEccN = 0x020000000000000000000000000000000000000000000000000000000000000000000000131850e1f19a63e4b391a8db917f4138b630d84be5d639381e91deb45cfe778f637c1001 | ||
605 | , curveEccH = 4 | ||
606 | } | ||
607 | typeSEC_t571r1 = CurveBinary $ CurveBinaryParam 0x080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000425 | ||
608 | paramSEC_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 #-} | ||
11 | module Crypto.Error.Types | ||
12 | ( CryptoError(..) | ||
13 | , CryptoFailable(..) | ||
14 | , throwCryptoErrorIO | ||
15 | , throwCryptoError | ||
16 | , onCryptoFailure | ||
17 | , eitherCryptoError | ||
18 | , maybeCryptoError | ||
19 | ) where | ||
20 | |||
21 | import qualified Control.Exception as E | ||
22 | import Data.Data | ||
23 | |||
24 | import Crypto.Internal.Imports | ||
25 | |||
26 | -- | Enumeration of all possible errors that can be found in this library | ||
27 | data 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 | |||
48 | instance 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 | -- | ||
58 | data CryptoFailable a = | ||
59 | CryptoPassed a | ||
60 | | CryptoFailed CryptoError | ||
61 | deriving (Show) | ||
62 | |||
63 | instance Eq a => Eq (CryptoFailable a) where | ||
64 | (==) (CryptoPassed a) (CryptoPassed b) = a == b | ||
65 | (==) (CryptoFailed e1) (CryptoFailed e2) = e1 == e2 | ||
66 | (==) _ _ = False | ||
67 | |||
68 | instance Functor CryptoFailable where | ||
69 | fmap f (CryptoPassed a) = CryptoPassed (f a) | ||
70 | fmap _ (CryptoFailed r) = CryptoFailed r | ||
71 | |||
72 | instance Applicative CryptoFailable where | ||
73 | pure a = CryptoPassed a | ||
74 | (<*>) fm m = fm >>= \p -> m >>= \r2 -> return (p r2) | ||
75 | instance 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 | ||
84 | throwCryptoErrorIO :: CryptoFailable a -> IO a | ||
85 | throwCryptoErrorIO (CryptoFailed e) = E.throwIO e | ||
86 | throwCryptoErrorIO (CryptoPassed r) = return r | ||
87 | |||
88 | -- | Same as 'throwCryptoErrorIO' but throw the error asynchronously. | ||
89 | throwCryptoError :: CryptoFailable a -> a | ||
90 | throwCryptoError (CryptoFailed e) = E.throw e | ||
91 | throwCryptoError (CryptoPassed r) = r | ||
92 | |||
93 | -- | Simple 'either' like combinator for CryptoFailable type | ||
94 | onCryptoFailure :: (CryptoError -> r) -> (a -> r) -> CryptoFailable a -> r | ||
95 | onCryptoFailure onError _ (CryptoFailed e) = onError e | ||
96 | onCryptoFailure _ onSuccess (CryptoPassed r) = onSuccess r | ||
97 | |||
98 | -- | Transform a CryptoFailable to an Either | ||
99 | eitherCryptoError :: CryptoFailable a -> Either CryptoError a | ||
100 | eitherCryptoError (CryptoFailed e) = Left e | ||
101 | eitherCryptoError (CryptoPassed a) = Right a | ||
102 | |||
103 | -- | Transform a CryptoFailable to a Maybe | ||
104 | maybeCryptoError :: CryptoFailable a -> Maybe a | ||
105 | maybeCryptoError (CryptoFailed _) = Nothing | ||
106 | maybeCryptoError (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 #-} | ||
11 | module Crypto.Internal.ByteArray | ||
12 | ( module Data.ByteArray | ||
13 | , module Data.ByteArray.Mapping | ||
14 | , module Data.ByteArray.Encoding | ||
15 | ) where | ||
16 | |||
17 | import Data.ByteArray | ||
18 | import Data.ByteArray.Mapping | ||
19 | import 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 #-} | ||
12 | module Crypto.Internal.Compat | ||
13 | ( unsafeDoIO | ||
14 | , popCount | ||
15 | , byteSwap64 | ||
16 | ) where | ||
17 | |||
18 | import System.IO.Unsafe | ||
19 | import Data.Word | ||
20 | import 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. | ||
27 | unsafeDoIO :: IO a -> a | ||
28 | #if __GLASGOW_HASKELL__ > 704 | ||
29 | unsafeDoIO = unsafeDupablePerformIO | ||
30 | #else | ||
31 | unsafeDoIO = unsafePerformIO | ||
32 | #endif | ||
33 | |||
34 | #if !(MIN_VERSION_base(4,5,0)) | ||
35 | popCount :: Word64 -> Int | ||
36 | popCount 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)) | ||
42 | byteSwap64 :: Word64 -> Word64 | ||
43 | byteSwap64 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 #-} | ||
13 | module Crypto.Internal.DeepSeq | ||
14 | ( NFData(..) | ||
15 | ) where | ||
16 | |||
17 | #ifdef WITH_DEEPSEQ_SUPPORT | ||
18 | import Control.DeepSeq | ||
19 | #else | ||
20 | import Data.Word | ||
21 | import Data.ByteArray | ||
22 | |||
23 | class NFData a where rnf :: a -> () | ||
24 | |||
25 | instance NFData Word8 where rnf w = w `seq` () | ||
26 | instance NFData Word16 where rnf w = w `seq` () | ||
27 | instance NFData Word32 where rnf w = w `seq` () | ||
28 | instance NFData Word64 where rnf w = w `seq` () | ||
29 | |||
30 | instance NFData Bytes where rnf b = b `seq` () | ||
31 | instance 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 | -- | ||
8 | module Crypto.Internal.Imports | ||
9 | ( module X | ||
10 | ) where | ||
11 | |||
12 | import Data.Word as X | ||
13 | import Control.Applicative as X | ||
14 | import Control.Monad as X (forM, forM_, void) | ||
15 | import Control.Arrow as X (first, second) | ||
16 | import 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 #-} | ||
13 | module 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 | |||
27 | import Data.Bits | ||
28 | import Data.Word | ||
29 | import Foreign.Ptr | ||
30 | import Foreign.Storable | ||
31 | import GHC.Ptr | ||
32 | |||
33 | -- import Crypto.Error | ||
34 | import Crypto.Error.Types | ||
35 | import Crypto.Internal.Compat | ||
36 | import Crypto.Internal.Imports | ||
37 | import Crypto.Internal.ByteArray (ByteArrayAccess, ByteArray, ScrubbedBytes, Bytes, withByteArray) | ||
38 | import qualified Crypto.Internal.ByteArray as B | ||
39 | -- import Crypto.Error (CryptoFailable(..)) | ||
40 | import Crypto.Random | ||
41 | |||
42 | -- | A Curve25519 Secret key | ||
43 | newtype SecretKey = SecretKey ScrubbedBytes | ||
44 | deriving (Show,Eq,ByteArrayAccess,NFData) | ||
45 | |||
46 | -- | A Curve25519 public key | ||
47 | newtype 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. | ||
52 | newtype DhSecret = DhSecret ScrubbedBytes | ||
53 | deriving (Show,Eq,ByteArrayAccess,NFData) | ||
54 | |||
55 | -- | Try to build a public key from a bytearray | ||
56 | publicKey :: ByteArrayAccess bs => bs -> CryptoFailable PublicKey | ||
57 | publicKey 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 | ||
62 | secretKey :: ByteArrayAccess bs => bs -> CryptoFailable SecretKey | ||
63 | secretKey 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 | ||
91 | dhSecret :: ByteArrayAccess b => b -> CryptoFailable DhSecret | ||
92 | dhSecret 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 | ||
97 | dh :: PublicKey -> SecretKey -> DhSecret | ||
98 | dh (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 | ||
106 | toPublic :: SecretKey -> PublicKey | ||
107 | toPublic (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. | ||
116 | generateSecretKey :: MonadRandom m => m SecretKey | ||
117 | generateSecretKey = 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 | |||
127 | foreign 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 @@ | |||
1 | name: dht-client | ||
2 | version: 0.0.0.4 | ||
3 | license: BSD3 | ||
4 | license-file: LICENSE | ||
5 | author: Joe Crayne | ||
6 | maintainer: Joe Crayne | ||
7 | copyright: (c) 2017 Joe Crayne, (c) 2013, Sam Truzjan | ||
8 | category: Network | ||
9 | build-type: Custom | ||
10 | cabal-version: >= 1.10 | ||
11 | tested-with: GHC == 8.0.2, GHC == 7.10.3 | ||
12 | homepage: https://github.com/cobit/bittorrent | ||
13 | bug-reports: https://github.com/cobit/bittorrent/issues | ||
14 | synopsis: BitTorrent DHT protocol implementation. | ||
15 | description: | ||
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 | |||
22 | extra-source-files: res/dapper-dvd-amd64.iso.torrent | ||
23 | res/pkg.torrent | ||
24 | README.md | ||
25 | ChangeLog | ||
26 | cbits/*.h | ||
27 | |||
28 | |||
29 | source-repository head | ||
30 | type: git | ||
31 | location: git://github.com/cobit/bittorrent.git | ||
32 | |||
33 | source-repository this | ||
34 | type: git | ||
35 | location: git://github.com/cobit/bittorrent.git | ||
36 | branch: master | ||
37 | tag: v0.0.0.4 | ||
38 | |||
39 | flag network-uri | ||
40 | description: Use network-uri package. | ||
41 | default: True | ||
42 | |||
43 | flag 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 | |||
51 | flag thread-debug | ||
52 | description: Add instrumentation to threads. | ||
53 | default: True | ||
54 | |||
55 | library | ||
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 | |||
186 | executable dht | ||
187 | hs-source-dirs: examples | ||
188 | main-is: dht.hs | ||
189 | default-language: Haskell2010 | ||
190 | build-depends: base, haskeline, network, bytestring, transformers | ||
191 | |||
192 | executable 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 #-} | ||
2 | import Control.Applicative | ||
3 | import Control.Monad | ||
4 | import Data.Function | ||
5 | import Control.Monad.IO.Class | ||
6 | import Data.Char | ||
7 | import Data.List | ||
8 | import Network.Socket as Socket | ||
9 | import System.Console.Haskeline | ||
10 | import System.Environment | ||
11 | import System.Exit | ||
12 | import System.IO | ||
13 | import System.IO.Unsafe | ||
14 | import 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. | ||
18 | hReadDigit :: Handle -> IO (Maybe Char) | ||
19 | hReadDigit 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. | ||
25 | hReadInt :: Handle -> IO Int | ||
26 | hReadInt 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. | ||
39 | readResponse :: Handle -> IO (Char, String) | ||
40 | readResponse 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. | ||
47 | sendCommand :: Handle -> String -> InputT IO () | ||
48 | sendCommand 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". | ||
57 | interactiveMode :: Handle -> InputT IO () -> InputT IO () | ||
58 | interactiveMode 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 | |||
65 | main :: IO () | ||
66 | main = 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 | |||
19 | module Main where | ||
20 | |||
21 | import Control.Arrow | ||
22 | import Control.Applicative | ||
23 | import Control.Concurrent.STM | ||
24 | import Control.DeepSeq | ||
25 | import Control.Exception | ||
26 | import Control.Monad | ||
27 | import Data.Bool | ||
28 | import Data.Char | ||
29 | import Data.Hashable | ||
30 | import Data.List | ||
31 | import qualified Data.IntMap.Strict as IntMap | ||
32 | import qualified Data.Map.Strict as Map | ||
33 | import Data.Maybe | ||
34 | import qualified Data.Set as Set | ||
35 | import Data.Time.Clock | ||
36 | import GHC.Conc (threadStatus,ThreadStatus(..)) | ||
37 | import GHC.Stats | ||
38 | import Network.Socket | ||
39 | import System.Environment | ||
40 | import System.IO | ||
41 | import System.Mem | ||
42 | import System.Posix.Process | ||
43 | import Text.PrettyPrint.HughesPJClass | ||
44 | import Text.Printf | ||
45 | import Text.Read | ||
46 | #ifdef THREAD_DEBUG | ||
47 | import Control.Concurrent.Lifted.Instrument | ||
48 | #else | ||
49 | import Control.Concurrent.Lifted | ||
50 | import GHC.Conc (labelThread) | ||
51 | #endif | ||
52 | import qualified Data.HashMap.Strict as HashMap | ||
53 | import qualified Data.Vector as V | ||
54 | import qualified Data.Text as T | ||
55 | import qualified Data.Text.Encoding as T | ||
56 | |||
57 | import Announcer | ||
58 | import Crypto.Tox -- (zeros32,SecretKey,PublicKey, generateSecretKey, toPublic, encodeSecret, decodeSecret, userKeys) | ||
59 | import Network.UPNP as UPNP | ||
60 | import Network.Address hiding (NodeId, NodeInfo(..)) | ||
61 | import Network.QueryResponse | ||
62 | import Network.StreamServer | ||
63 | import Network.Kademlia | ||
64 | import Network.Kademlia.Bootstrap | ||
65 | import Network.Kademlia.Search | ||
66 | import qualified Network.BitTorrent.MainlineDHT as Mainline | ||
67 | import qualified Network.Tox as Tox | ||
68 | import Network.Kademlia.Routing as R | ||
69 | import Data.Aeson as J (ToJSON, FromJSON) | ||
70 | import qualified Data.Aeson as J | ||
71 | import qualified Data.ByteString.Lazy as L | ||
72 | import qualified Data.ByteString.Char8 as B | ||
73 | import Control.Concurrent.Tasks | ||
74 | import System.IO.Error | ||
75 | import qualified Data.Serialize as S | ||
76 | import Network.BitTorrent.DHT.ContactInfo as Peers | ||
77 | import qualified Data.MinMaxPSQ as MM | ||
78 | import Data.Wrapper.PSQ as PSQ (pattern (:->)) | ||
79 | import qualified Data.Wrapper.PSQ as PSQ | ||
80 | import Data.Ord | ||
81 | import Data.Time.Clock.POSIX | ||
82 | import qualified Network.Tox.DHT.Transport as Tox | ||
83 | import qualified Network.Tox.DHT.Handlers as Tox | ||
84 | import qualified Network.Tox.Onion.Transport as Tox | ||
85 | import qualified Network.Tox.Onion.Handlers as Tox | ||
86 | import qualified Network.Tox.Crypto.Handlers as Tox | ||
87 | import Data.Typeable | ||
88 | import Roster | ||
89 | import OnionRouter | ||
90 | |||
91 | showReport :: [(String,String)] -> String | ||
92 | showReport kvs = showColumns $ map (\(x,y)->[x,y]) kvs | ||
93 | |||
94 | showColumns :: [[String]] -> String | ||
95 | showColumns 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 | |||
103 | marshalForClient :: String -> String | ||
104 | marshalForClient s = show (length s) ++ ":" ++ s | ||
105 | |||
106 | data ClientHandle = ClientHandle Handle (MVar Int) | ||
107 | |||
108 | -- | Writes a message and signals ready for next command. | ||
109 | hPutClient :: ClientHandle -> String -> IO () | ||
110 | hPutClient (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. | ||
116 | hPutClientChunk :: ClientHandle -> String -> IO () | ||
117 | hPutClientChunk (ClientHandle h hstate) s = do | ||
118 | st <- takeMVar hstate | ||
119 | hPutStr h (' ' : marshalForClient s) | ||
120 | putMVar hstate 2 -- ready for more output | ||
121 | |||
122 | data 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 | |||
134 | data 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 | |||
149 | data 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 | |||
164 | data 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 | |||
171 | data DHTPing ni = forall r. DHTPing | ||
172 | { pingQuery :: [String] -> ni -> IO (Maybe r) | ||
173 | , pingShowResult :: r -> String | ||
174 | } | ||
175 | |||
176 | data 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 | |||
201 | nodesFileName :: String -> String | ||
202 | nodesFileName netname = netname ++ "-nodes.json" | ||
203 | |||
204 | saveNodes :: String -> DHT -> IO () | ||
205 | saveNodes 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 | |||
212 | loadNodes :: FromJSON ni => String -> IO [ni] | ||
213 | loadNodes 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 | |||
220 | fallbackLoad :: FromJSON t => FilePath -> IO [t] | ||
221 | fallbackLoad 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 | {- | ||
239 | pingNodes :: String -> DHT -> IO Bool | ||
240 | pingNodes 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 | |||
247 | asProxyTypeOf :: a -> proxy a -> a | ||
248 | asProxyTypeOf = const | ||
249 | |||
250 | pingNodes :: String -> DHT -> IO (Maybe Int) | ||
251 | pingNodes 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 | ||
267 | pingNodes _ _ = return Nothing | ||
268 | |||
269 | |||
270 | |||
271 | reportTable :: Show ni => BucketList ni -> [(String,String)] | ||
272 | reportTable bkts = map (show *** show . fst) | ||
273 | $ concat | ||
274 | $ zipWith map (map (,) [0::Int ..]) | ||
275 | $ R.toList | ||
276 | $ bkts | ||
277 | |||
278 | reportResult :: | ||
279 | String | ||
280 | -> (r -> String) | ||
281 | -> (tok -> Maybe String) | ||
282 | -> (ni -> String) | ||
283 | -> ClientHandle | ||
284 | -> Either String ([ni],[r],Maybe tok) | ||
285 | -> IO () | ||
286 | reportResult meth showR showTok showN h (Left e) = hPutClient h e | ||
287 | reportResult 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 | ||
307 | showSearches :: ( Show nid | ||
308 | , Ord nid | ||
309 | , Hashable nid | ||
310 | , Ord ni | ||
311 | , Hashable ni | ||
312 | ) => Map.Map (String,nid) (DHTSearch nid ni) -> IO String | ||
313 | showSearches 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 | |||
331 | forkSearch :: | ||
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 () | ||
346 | forkSearch 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 | |||
363 | reportSearchResults :: (Show t, Ord t1, Ord t, Hashable t) => | ||
364 | String -> ClientHandle -> DHTSearch t1 t -> IO () | ||
365 | reportSearchResults 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 | |||
378 | data 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 | |||
392 | exceptionsToClient :: ClientHandle -> IO () -> IO () | ||
393 | exceptionsToClient (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 | |||
400 | hGetClientLine :: ClientHandle -> IO String | ||
401 | hGetClientLine (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 | |||
408 | hCloseClient :: ClientHandle -> IO () | ||
409 | hCloseClient (ClientHandle h hstate) = do | ||
410 | st <- takeMVar hstate | ||
411 | hClose h | ||
412 | putMVar hstate 3 -- closed file handle | ||
413 | |||
414 | clientSession0 :: Session -> t1 -> t -> Handle -> IO () | ||
415 | clientSession0 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 | |||
421 | clientSession :: Session -> t1 -> t -> ClientHandle -> IO () | ||
422 | clientSession 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 | |||
950 | readExternals :: (ni -> SockAddr) -> [TVar (BucketList ni)] -> IO [SockAddr] | ||
951 | readExternals 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 | |||
959 | data Options = Options | ||
960 | { portbt :: String | ||
961 | , porttox :: String | ||
962 | , ip6bt :: Bool | ||
963 | , ip6tox :: Bool | ||
964 | , dhtkey :: Maybe SecretKey | ||
965 | } | ||
966 | deriving (Eq,Show) | ||
967 | |||
968 | sensibleDefaults :: Options | ||
969 | sensibleDefaults = 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 | ||
979 | parseArgs :: [String] -> Options -> Options | ||
980 | parseArgs [] opts = opts | ||
981 | parseArgs ("--dhtkey":k:args) opts = parseArgs args opts | ||
982 | { dhtkey = decodeSecret $ B.pack k } | ||
983 | parseArgs ("-4":args) opts = parseArgs args opts | ||
984 | { ip6bt = False | ||
985 | , ip6tox = False } | ||
986 | parseArgs (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 | |||
994 | noArgPing :: (x -> IO (Maybe r)) -> [String] -> x -> IO (Maybe r) | ||
995 | noArgPing f [] x = f x | ||
996 | noArgPing _ _ _ = return Nothing | ||
997 | |||
998 | main :: IO () | ||
999 | main = 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 #-} | ||
2 | module Main where | ||
3 | |||
4 | import Data.Binary.Get (runGet) | ||
5 | import qualified Data.ByteString as BS | ||
6 | import qualified Data.ByteString as B | ||
7 | import qualified Data.ByteString.Lazy as LZ | ||
8 | import qualified Data.ByteString.Lazy.Char8 as L8 | ||
9 | import Data.IORef | ||
10 | import Data.List | ||
11 | import Debug.Trace | ||
12 | import Text.Printf | ||
13 | import Text.Show.Pretty as PP | ||
14 | import "network-house" Net.Packet | ||
15 | import qualified "network-house" Net.IPv4 as IP4 | ||
16 | import qualified "network-house" Net.IPv6 as IP6 | ||
17 | import "network-house" Net.PacketParsing | ||
18 | import "network-house" Net.UDP as UDP | ||
19 | import "pcap" Network.Pcap | ||
20 | import qualified Data.Serialize as S | ||
21 | import qualified Network.Socket as HS | ||
22 | import Control.Applicative | ||
23 | |||
24 | import Crypto.Tox | ||
25 | import Network.Tox.DHT.Transport as Tox | ||
26 | import Data.BEncode as BE | ||
27 | import Data.BEncode.Pretty | ||
28 | -- import Data.IKE.Message | ||
29 | |||
30 | -- traceM string = trace string $ return () | ||
31 | |||
32 | bs2chunk :: BS.ByteString -> UArray Int Word8 | ||
33 | bs2chunk bs = listArray (0,subtract 33 $ BS.length bs) $ drop 32 $ BS.unpack bs | ||
34 | |||
35 | hex :: BS.ByteString -> String | ||
36 | hex = concatMap (printf "%02x") . B.unpack | ||
37 | |||
38 | hexlines :: BS.ByteString -> [String] | ||
39 | hexlines 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 | |||
44 | parsePacket :: IORef Int -> PktHdr -> BS.ByteString -> IO () | ||
45 | parsePacket 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 | |||
77 | main = 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 @@ | |||
1 | module Control.Concurrent.Async.Lifted.Instrument | ||
2 | ( module Control.Concurrent.Async.Lifted | ||
3 | ) where | ||
4 | |||
5 | import 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 #-} | ||
2 | module Control.Concurrent.Lifted.Instrument | ||
3 | ( module Control.Concurrent.Lifted | ||
4 | , forkIO | ||
5 | , fork | ||
6 | , labelThread | ||
7 | , threadsInformation | ||
8 | , PerThread(..) | ||
9 | ) where | ||
10 | |||
11 | import qualified Control.Concurrent.Lifted as Raw | ||
12 | import Control.Concurrent.Lifted hiding (fork) | ||
13 | import Control.Exception (fromException) | ||
14 | import Control.Monad.Trans.Control | ||
15 | import System.IO.Unsafe | ||
16 | import qualified Data.Map.Strict as Map | ||
17 | import Control.Exception.Lifted | ||
18 | import Control.Monad.Base | ||
19 | import qualified GHC.Conc as GHC | ||
20 | import Data.Time() | ||
21 | import Data.Time.Clock | ||
22 | import System.IO | ||
23 | import Control.Monad.IO.Class | ||
24 | |||
25 | |||
26 | data PerThread = PerThread | ||
27 | { lbl :: String | ||
28 | , startTime :: UTCTime | ||
29 | } | ||
30 | deriving (Eq,Ord,Show) | ||
31 | |||
32 | data GlobalState = GlobalState | ||
33 | { threads :: !(Map.Map ThreadId PerThread) | ||
34 | , reportException :: String -> IO () | ||
35 | } | ||
36 | |||
37 | globals :: MVar GlobalState | ||
38 | globals = unsafePerformIO $ newMVar $ GlobalState | ||
39 | { threads = Map.empty | ||
40 | , reportException = hPutStrLn stderr | ||
41 | } | ||
42 | {-# NOINLINE globals #-} | ||
43 | |||
44 | |||
45 | forkIO :: IO () -> IO ThreadId | ||
46 | forkIO = fork | ||
47 | {-# INLINE forkIO #-} | ||
48 | |||
49 | fork :: (MonadBaseControl IO m, MonadIO m) => m () -> m ThreadId | ||
50 | fork 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 | |||
72 | labelThread :: ThreadId -> String -> IO () | ||
73 | labelThread tid s = do | ||
74 | GHC.labelThread tid s | ||
75 | modifyThreads $! Map.adjust (\pt -> pt { lbl = s }) tid | ||
76 | {-# INLINE labelThread #-} | ||
77 | |||
78 | threadsInformation :: IO [(ThreadId,PerThread)] | ||
79 | threadsInformation = do | ||
80 | m <- threads <$> readMVar globals | ||
81 | return $ Map.toList m | ||
82 | |||
83 | |||
84 | modifyThreads :: MonadBase IO m => (Map.Map ThreadId PerThread -> Map.Map ThreadId PerThread) -> m () | ||
85 | modifyThreads 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 #-} | ||
2 | module Control.Concurrent.Tasks where | ||
3 | |||
4 | import Control.Concurrent.STM | ||
5 | import Control.Exception | ||
6 | import Data.Function | ||
7 | import Data.List | ||
8 | #ifdef THREAD_DEBUG | ||
9 | import Control.Concurrent.Lifted.Instrument | ||
10 | #else | ||
11 | import Control.Concurrent.Lifted | ||
12 | import GHC.Conc (labelThread) | ||
13 | #endif | ||
14 | |||
15 | newtype TaskGroup = TaskGroup | ||
16 | { taskQueue :: TChan (String,IO ()) | ||
17 | } | ||
18 | |||
19 | withTaskGroup :: String -> Int -> (TaskGroup -> IO ()) -> IO () | ||
20 | withTaskGroup 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 | |||
43 | forkTask :: TaskGroup -> String -> IO () -> IO () | ||
44 | forkTask (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 #-} | ||
2 | module Control.TriadCommittee where | ||
3 | |||
4 | import Control.Concurrent.STM | ||
5 | import Control.Monad | ||
6 | import Data.Maybe | ||
7 | |||
8 | |||
9 | data TriadSlot = SlotA | SlotB | SlotC | ||
10 | deriving (Eq,Ord,Enum,Show,Read) | ||
11 | |||
12 | data 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 | |||
20 | triadSlot :: TriadSlot -> TriadCommittee voter a -> TVar (Maybe (voter,a)) | ||
21 | triadSlot SlotA = triadA | ||
22 | triadSlot SlotB = triadB | ||
23 | triadSlot SlotC = triadC | ||
24 | |||
25 | triadDecision :: a -> TriadCommittee voter a -> STM a | ||
26 | triadDecision fallback triad = do | ||
27 | slot <- readTVar (triadDecider triad) | ||
28 | maybe fallback snd <$> readTVar (triadSlot slot triad) | ||
29 | |||
30 | |||
31 | newTriadCommittee :: (a -> STM ()) -> STM (TriadCommittee voter a) | ||
32 | newTriadCommittee onChange = | ||
33 | TriadCommittee <$> newTVar SlotA | ||
34 | <*> newTVar Nothing | ||
35 | <*> newTVar Nothing | ||
36 | <*> newTVar Nothing | ||
37 | <*> pure onChange | ||
38 | |||
39 | |||
40 | triadCountVotes :: Eq a => Maybe a -> TriadCommittee voter a -> STM () | ||
41 | triadCountVotes 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 | |||
58 | addVote :: (Eq voter, Eq a) => TriadCommittee voter a -> voter -> a -> STM () | ||
59 | addVote 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 | |||
75 | delVote :: (Eq voter, Eq a) => TriadCommittee voter a -> voter -> STM () | ||
76 | delVote 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 #-} | ||
12 | module 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 | |||
58 | import Control.Arrow | ||
59 | import Control.Monad | ||
60 | import qualified Crypto.Cipher.ChaChaPoly1305 as Symmetric | ||
61 | import qualified Crypto.Cipher.Salsa as Salsa | ||
62 | import qualified Crypto.Cipher.XSalsa as XSalsa | ||
63 | import Crypto.ECC.Class | ||
64 | import qualified Crypto.Error as Cryptonite | ||
65 | import qualified Crypto.MAC.Poly1305 as Poly1305 | ||
66 | import Crypto.PubKey.Curve25519 | ||
67 | import Data.Bits | ||
68 | import qualified Data.ByteArray as BA | ||
69 | ;import Data.ByteArray as BA (ByteArrayAccess, Bytes) | ||
70 | import Data.ByteString as B | ||
71 | import qualified Data.ByteString.Base16 as Base16 | ||
72 | import qualified Data.ByteString.Base64 as Base64 | ||
73 | import qualified Data.ByteString.Char8 as C8 | ||
74 | import Data.Data | ||
75 | import Data.Functor.Contravariant | ||
76 | #if MIN_VERSION_base(4,9,1) | ||
77 | import Data.Kind | ||
78 | #else | ||
79 | import GHC.Exts (Constraint) | ||
80 | #endif | ||
81 | import Data.Ord | ||
82 | import Data.Serialize as S | ||
83 | import Data.Word | ||
84 | import Foreign.Marshal.Alloc | ||
85 | import Foreign.Ptr | ||
86 | import Foreign.Storable | ||
87 | import System.Endian | ||
88 | import qualified Data.ByteString.Internal | ||
89 | import Control.Concurrent.STM | ||
90 | import Crypto.Error.Types (CryptoFailable (..), throwCryptoError) | ||
91 | import Network.Socket (SockAddr) | ||
92 | import GHC.Exts (Word(..),inline) | ||
93 | import GHC.Prim | ||
94 | import Data.Word64Map (fitsInInt) | ||
95 | import Data.MinMaxPSQ (MinMaxPSQ') | ||
96 | import qualified Data.MinMaxPSQ as MM | ||
97 | import Data.Time.Clock.POSIX | ||
98 | import Data.Hashable | ||
99 | import System.IO.Unsafe (unsafeDupablePerformIO) | ||
100 | |||
101 | -- | A 16-byte mac and an arbitrary-length encrypted stream. | ||
102 | newtype Encrypted a = Encrypted ByteString | ||
103 | deriving (Eq,Ord,Data,ByteArrayAccess) | ||
104 | |||
105 | newtype Encrypted8 a = E8 (Encrypted (a,Nonce8)) | ||
106 | deriving (Serialize, Show) | ||
107 | |||
108 | newtype (f ∘ g) x = Composed { uncomposed :: f (g x) } | ||
109 | |||
110 | infixr ∘ | ||
111 | |||
112 | newtype Auth = Auth Poly1305.Auth deriving (Eq, ByteArrayAccess) | ||
113 | instance Ord Auth where | ||
114 | compare (Auth a) (Auth b) = comparing (BA.convert :: Poly1305.Auth -> Bytes) a b | ||
115 | instance 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] | ||
121 | con_Auth :: Constr | ||
122 | con_Auth = mkConstr (dataTypeOf (Auth (error "con_Auth"))) "Auth" [] Prefix | ||
123 | instance Serialize Auth where | ||
124 | get = Auth . Poly1305.Auth . BA.convert <$> getBytes 16 | ||
125 | put (Auth (Poly1305.Auth bs)) = putByteString $ BA.convert bs | ||
126 | |||
127 | instance Typeable a => Show (Encrypted a) where | ||
128 | show (Encrypted _) = "Encrypted "++show (typeOf (undefined :: a)) | ||
129 | |||
130 | encryptedAuth :: Encrypted a -> Auth | ||
131 | encryptedAuth (Encrypted bs) | ||
132 | | Right auth <- decode (B.take 16 bs) = auth | ||
133 | | otherwise = error "encryptedAuth: insufficient bytes" | ||
134 | |||
135 | authAndBytes :: Encrypted a -> (Auth, ByteString) | ||
136 | authAndBytes (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. | ||
143 | data Size a | ||
144 | = VarSize (a -> Int) | ||
145 | | ConstSize !Int | ||
146 | deriving Typeable | ||
147 | |||
148 | instance Contravariant Size where | ||
149 | contramap f sz = case sz of | ||
150 | ConstSize n -> ConstSize n | ||
151 | VarSize g -> VarSize (\x -> g (f x)) | ||
152 | |||
153 | instance 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 | |||
161 | class Sized a where size :: Size a | ||
162 | |||
163 | instance 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 | |||
169 | instance 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 | |||
174 | instance (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 | |||
181 | getRemainingEncrypted :: Get (Encrypted a) | ||
182 | getRemainingEncrypted = Encrypted <$> (remaining >>= getBytes) | ||
183 | |||
184 | putEncrypted :: Encrypted a -> Put | ||
185 | putEncrypted (Encrypted bs) = putByteString bs | ||
186 | |||
187 | newtype Plain (s:: * -> Constraint) a = Plain ByteString | ||
188 | |||
189 | |||
190 | decodePlain :: Serialize a => Plain Serialize a -> Either String a | ||
191 | decodePlain (Plain bs) = decode bs | ||
192 | |||
193 | encodePlain :: Serialize a => a -> Plain Serialize a | ||
194 | encodePlain a = Plain $ encode a | ||
195 | |||
196 | storePlain :: Storable a => a -> IO (Plain Storable a) | ||
197 | storePlain a = Plain <$> BA.create (sizeOf a) (`poke` a) | ||
198 | |||
199 | retrievePlain :: Storable a => Plain Storable a -> IO a | ||
200 | retrievePlain (Plain bs) = BA.withByteArray bs peek | ||
201 | |||
202 | decryptSymmetric :: SymmetricKey -> Nonce24 -> Encrypted a -> Either String (Plain s a) | ||
203 | decryptSymmetric (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 | |||
215 | encryptSymmetric :: SymmetricKey -> Nonce24 -> Plain s x -> Encrypted x | ||
216 | encryptSymmetric (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 | |||
226 | data State = State Poly1305.State XSalsa.State | ||
227 | |||
228 | decrypt :: State -> Encrypted a -> Either String (Plain s a) | ||
229 | decrypt (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 | ||
239 | encrypt :: State -> Plain s a -> Encrypted a | ||
240 | encrypt (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) | ||
246 | computeSharedSecret :: SecretKey -> PublicKey -> Nonce24 -> State | ||
247 | computeSharedSecret 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 | |||
261 | unsafeFirstWord64 :: ByteArrayAccess ba => ba -> Word64 | ||
262 | unsafeFirstWord64 ba = unsafeDupablePerformIO $ BA.withByteArray ba peek | ||
263 | {-# INLINE unsafeFirstWord64 #-} | ||
264 | |||
265 | instance Hashable PublicKey where | ||
266 | hashWithSalt salt pk = hashWithSalt salt (unsafeFirstWord64 pk) | ||
267 | {-# INLINE hashWithSalt #-} | ||
268 | |||
269 | instance Hashable SecretKey where | ||
270 | hashWithSalt salt sk = hashWithSalt salt (unsafeFirstWord64 sk) | ||
271 | {-# INLINE hashWithSalt #-} | ||
272 | |||
273 | instance Ord PublicKey where compare = unsafeCompare32Bytes | ||
274 | {-# INLINE compare #-} | ||
275 | instance Ord SecretKey where compare = unsafeCompare32Bytes | ||
276 | {-# INLINE compare #-} | ||
277 | |||
278 | unsafeCompare32Bytes :: (ByteArrayAccess ba, ByteArrayAccess bb) | ||
279 | => ba -> bb -> Ordering | ||
280 | unsafeCompare32Bytes ba bb = | ||
281 | unsafeDupablePerformIO $ BA.withByteArray ba | ||
282 | $ \pa -> BA.withByteArray bb | ||
283 | $ \pb -> unsafeCompare32Bytes' 3 pa pb | ||
284 | |||
285 | unsafeCompare32Bytes' :: Int -> Ptr Word64 -> Ptr Word64 -> IO Ordering | ||
286 | unsafeCompare32Bytes' !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 | |||
299 | lookupSharedSecret :: TransportCrypto -> SecretKey -> PublicKey -> Nonce24 -> IO State | ||
300 | lookupSharedSecret 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 | |||
320 | hsalsa20 :: (ByteArrayAccess t, ByteArrayAccess t1) => t1 -> t -> BA.ScrubbedBytes | ||
321 | hsalsa20 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 | |||
330 | newtype Nonce24 = Nonce24 ByteString | ||
331 | deriving (Eq, Ord, ByteArrayAccess,Data) | ||
332 | |||
333 | addtoNonce24 :: Nonce24 -> Word -> IO Nonce24 | ||
334 | addtoNonce24 (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 | |||
375 | incrementNonce24 :: Nonce24 -> IO Nonce24 | ||
376 | incrementNonce24 nonce24 = addtoNonce24 nonce24 1 | ||
377 | |||
378 | quoted :: ShowS -> ShowS | ||
379 | quoted shows s = '"':shows ('"':s) | ||
380 | |||
381 | bin2hex :: ByteArrayAccess bs => bs -> String | ||
382 | bin2hex = C8.unpack . Base16.encode . BA.convert | ||
383 | |||
384 | bin2base64 :: ByteArrayAccess bs => bs -> String | ||
385 | bin2base64 = C8.unpack . Base64.encode . BA.convert | ||
386 | |||
387 | |||
388 | instance Show Nonce24 where | ||
389 | showsPrec d nonce = quoted (mappend $ bin2hex nonce) | ||
390 | |||
391 | instance Sized Nonce24 where size = ConstSize 24 | ||
392 | |||
393 | instance Serialize Nonce24 where | ||
394 | get = Nonce24 <$> getBytes 24 | ||
395 | put (Nonce24 bs) = putByteString bs | ||
396 | |||
397 | newtype Nonce8 = Nonce8 Word64 | ||
398 | deriving (Eq, Ord, Data, Serialize) | ||
399 | |||
400 | -- Note: Big-endian to match Serialize instance. | ||
401 | instance 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 | |||
407 | instance Sized Nonce8 where size = ConstSize 8 | ||
408 | |||
409 | instance 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 | |||
416 | instance Show Nonce8 where | ||
417 | showsPrec d nonce = quoted (mappend $ bin2hex nonce) | ||
418 | |||
419 | |||
420 | newtype Nonce32 = Nonce32 ByteString | ||
421 | deriving (Eq, Ord, ByteArrayAccess, Data) | ||
422 | |||
423 | instance Show Nonce32 where | ||
424 | showsPrec d nonce = mappend $ bin2base64 nonce | ||
425 | |||
426 | instance 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 | |||
436 | instance Serialize Nonce32 where | ||
437 | get = Nonce32 <$> getBytes 32 | ||
438 | put (Nonce32 bs) = putByteString bs | ||
439 | |||
440 | instance Sized Nonce32 where size = ConstSize 32 | ||
441 | |||
442 | |||
443 | zeros32 :: Nonce32 | ||
444 | zeros32 = Nonce32 $ BA.replicate 32 0 | ||
445 | |||
446 | zeros24 :: ByteString | ||
447 | zeros24 = BA.take 24 zs where Nonce32 zs = zeros32 | ||
448 | |||
449 | -- | `32` | sender's DHT public key | | ||
450 | -- | `24` | nonce | | ||
451 | -- | `?` | encrypted message | | ||
452 | data Asymm a = Asymm | ||
453 | { senderKey :: PublicKey | ||
454 | , asymmNonce :: Nonce24 | ||
455 | , asymmData :: a | ||
456 | } | ||
457 | deriving (Functor,Foldable,Traversable, Show) | ||
458 | |||
459 | instance 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. | ||
468 | getAsymm :: Serialize a => Get (Asymm a) | ||
469 | getAsymm = Asymm <$> getPublicKey <*> get <*> get | ||
470 | |||
471 | putAsymm :: Serialize a => Asymm a -> Put | ||
472 | putAsymm (Asymm key nonce dta) = putPublicKey key >> put nonce >> put dta | ||
473 | |||
474 | -- | Field order: nonce, and then senderKey. | ||
475 | getAliasedAsymm :: Serialize a => Get (Asymm a) | ||
476 | getAliasedAsymm = flip Asymm <$> get <*> getPublicKey <*> get | ||
477 | |||
478 | putAliasedAsymm :: Serialize a => Asymm a -> Put | ||
479 | putAliasedAsymm (Asymm key nonce dta) = put nonce >> putPublicKey key >> put dta | ||
480 | |||
481 | data SecretsCache = SecretsCache | ||
482 | { sharedSecret :: TVar (MinMaxPSQ' PublicKey | ||
483 | (Down POSIXTime) | ||
484 | (MinMaxPSQ' SecretKey (Down POSIXTime) (Nonce24 -> State))) | ||
485 | } | ||
486 | |||
487 | newSecretsCache :: IO SecretsCache | ||
488 | newSecretsCache = atomically (SecretsCache <$> newTVar MM.empty) | ||
489 | |||
490 | |||
491 | newtype SymmetricKey = SymmetricKey ByteString | ||
492 | |||
493 | data 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 | |||
507 | getPublicKey :: S.Get PublicKey | ||
508 | getPublicKey = throwCryptoError . publicKey <$> S.getBytes 32 | ||
509 | |||
510 | putPublicKey :: PublicKey -> S.Put | ||
511 | putPublicKey bs = S.putByteString $ BA.convert bs | ||
512 | |||
513 | -- 32 bytes -> 42 base64 digits. | ||
514 | -- | ||
515 | encodeSecret :: SecretKey -> Maybe C8.ByteString | ||
516 | encodeSecret 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. | ||
560 | decodeSecret :: C8.ByteString -> Maybe SecretKey | ||
561 | decodeSecret k64 | B.length k64 < 42 = Nothing | ||
562 | decodeSecret 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 #-} | ||
2 | module Data.BEncode.Pretty where -- (showBEncode) where | ||
3 | |||
4 | import Data.BEncode.Types | ||
5 | import qualified Data.ByteString as BS | ||
6 | import qualified Data.ByteString.Lazy as BL | ||
7 | import qualified Data.ByteString.Lazy.Char8 as BL8 | ||
8 | import Data.Text (Text) | ||
9 | import qualified Data.Text as T | ||
10 | import Data.Text.Encoding | ||
11 | import Text.Printf | ||
12 | import qualified Data.ByteString.Base16 as Base16 | ||
13 | #ifdef BENCODE_AESON | ||
14 | import Data.BEncode.BDict hiding (map) | ||
15 | import Data.Aeson.Types hiding (parse) | ||
16 | import Data.Aeson.Encode.Pretty | ||
17 | import qualified Data.HashMap.Strict as HashMap | ||
18 | import qualified Data.Vector as Vector | ||
19 | import Data.Foldable as Foldable | ||
20 | #endif | ||
21 | |||
22 | {- | ||
23 | unhex :: Text -> BS.ByteString | ||
24 | unhex 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 | |||
32 | hex :: BS.ByteString -> Text | ||
33 | hex bs = T.concat $ map (T.pack . printf "%02X") $ BS.unpack bs | ||
34 | -} | ||
35 | |||
36 | #ifdef BENCODE_AESON | ||
37 | |||
38 | quote_chr :: Char | ||
39 | quote_chr = ' ' | ||
40 | |||
41 | quote :: Text -> Text | ||
42 | quote t = quote_chr `T.cons` t `T.snoc` quote_chr | ||
43 | |||
44 | encodeByteString :: BS.ByteString -> Text | ||
45 | encodeByteString s = either (const . decodeUtf8 $ Base16.encode s) quote $ decodeUtf8' s | ||
46 | |||
47 | decodeByteString :: Text -> BS.ByteString | ||
48 | decodeByteString s | ||
49 | | T.head s==quote_chr = encodeUtf8 (T.takeWhile (/=quote_chr) $ T.drop 1 s) | ||
50 | | otherwise = fst (Base16.decode (encodeUtf8 s)) | ||
51 | |||
52 | instance 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 | |||
58 | instance 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 | |||
63 | instance 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 | |||
71 | instance 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 | |||
78 | showBEncode :: BValue -> BL.ByteString | ||
79 | #ifdef BENCODE_AESON | ||
80 | showBEncode b = encodePretty $ toJSON b | ||
81 | #else | ||
82 | showBEncode 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 | ------------------------------------------------------------------------------- | ||
13 | module Data.Bits.ByteString where | ||
14 | |||
15 | import Data.Bits | ||
16 | import qualified Data.ByteString as B | ||
17 | import Data.Word | ||
18 | |||
19 | instance 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 @@ | |||
1 | module Data.Digest.CRC32C | ||
2 | ( crc32c | ||
3 | , crc32c_update | ||
4 | ) where | ||
5 | |||
6 | import Data.Bits | ||
7 | import Data.ByteString (ByteString) | ||
8 | import Data.Word | ||
9 | import Data.Array.Base (unsafeAt) | ||
10 | import Data.Array.Unboxed | ||
11 | |||
12 | import qualified Data.ByteString as B | ||
13 | |||
14 | |||
15 | crc32c :: ByteString -> Word32 | ||
16 | crc32c = crc32c_update 0 | ||
17 | |||
18 | crc32c_update :: Word32 -> ByteString -> Word32 | ||
19 | crc32c_update crc bs = flipd $ step (flipd crc) bs | ||
20 | where | ||
21 | flipd = xor 0xffffffff | ||
22 | |||
23 | step :: Word32 -> ByteString -> Word32 | ||
24 | step 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 | ||
31 | arr !!! i = unsafeAt arr $ fromIntegral i | ||
32 | {-# INLINEABLE (!!!) #-} | ||
33 | |||
34 | table :: UArray Word32 Word32 | ||
35 | table = 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 #-} | ||
2 | module Data.MinMaxPSQ | ||
3 | ( module Data.MinMaxPSQ | ||
4 | , Binding' | ||
5 | , pattern Binding | ||
6 | ) where | ||
7 | |||
8 | import Data.Ord | ||
9 | import qualified Data.Wrapper.PSQ as PSQ | ||
10 | ;import Data.Wrapper.PSQ as PSQ hiding (insert, insert', null, size) | ||
11 | import Prelude hiding (null, take) | ||
12 | |||
13 | data MinMaxPSQ' k p v = MinMaxPSQ !(PSQ' k p v) !(PSQ' k (Down p) v) | ||
14 | type MinMaxPSQ k p = MinMaxPSQ' k p () | ||
15 | |||
16 | empty :: MinMaxPSQ' k p v | ||
17 | empty = MinMaxPSQ PSQ.empty PSQ.empty | ||
18 | |||
19 | singleton' :: (PSQKey k, Ord p) => k -> v -> p -> MinMaxPSQ' k p v | ||
20 | singleton' k v p = MinMaxPSQ (PSQ.singleton' k v p) (PSQ.singleton' k v (Down p)) | ||
21 | |||
22 | null :: MinMaxPSQ' k p v -> Bool | ||
23 | null (MinMaxPSQ nq xq) = PSQ.null nq | ||
24 | |||
25 | size :: MinMaxPSQ' k p v -> Int | ||
26 | size (MinMaxPSQ nq xq) = PSQ.size nq | ||
27 | |||
28 | toList :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> [Binding' k p v] | ||
29 | toList (MinMaxPSQ nq xq) = PSQ.toList nq | ||
30 | |||
31 | fromList :: (PSQKey k, Ord p) => [Binding' k p v] -> MinMaxPSQ' k p v | ||
32 | fromList kps = MinMaxPSQ (PSQ.fromList kps) | ||
33 | (PSQ.fromList $ map (\(Binding k v p) -> Binding k v (Down p)) kps) | ||
34 | |||
35 | findMin :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v) | ||
36 | findMin (MinMaxPSQ nq xq) = PSQ.findMin nq | ||
37 | |||
38 | findMax :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v) | ||
39 | findMax (MinMaxPSQ nq xq) = fmap (\(Binding k v (Down p)) -> Binding k v p) $ PSQ.findMin xq | ||
40 | |||
41 | insert :: (PSQKey k, Ord p) => k -> p -> MinMaxPSQ k p -> MinMaxPSQ k p | ||
42 | insert k p (MinMaxPSQ nq xq) = MinMaxPSQ (PSQ.insert k p nq) | ||
43 | (PSQ.insert k (Down p) xq) | ||
44 | |||
45 | insert' :: (PSQKey k, Ord p) => k -> v -> p -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v | ||
46 | insert' k v p (MinMaxPSQ nq xq) = MinMaxPSQ (PSQ.insert' k v p nq) | ||
47 | (PSQ.insert' k v (Down p) xq) | ||
48 | |||
49 | delete :: (PSQKey k, Ord p) => k -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v | ||
50 | delete k (MinMaxPSQ nq xq) = MinMaxPSQ (PSQ.delete k nq) (PSQ.delete k xq) | ||
51 | |||
52 | deleteMin :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> MinMaxPSQ' k p v | ||
53 | deleteMin (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 | |||
57 | deleteMax :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> MinMaxPSQ' k p v | ||
58 | deleteMax (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 | |||
62 | minView :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v, MinMaxPSQ' k p v) | ||
63 | minView (MinMaxPSQ nq xq) = fmap (\(Binding k v p, nq') -> (Binding k v p, MinMaxPSQ nq' (PSQ.delete k xq))) | ||
64 | $ PSQ.minView nq | ||
65 | |||
66 | maxView :: (PSQKey k, Ord p) => MinMaxPSQ' k p v -> Maybe (Binding' k p v, MinMaxPSQ' k p v) | ||
67 | maxView (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. | ||
72 | insertTake :: (PSQKey k, Ord p) => Int -> k -> p -> MinMaxPSQ k p -> MinMaxPSQ k p | ||
73 | insertTake 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. | ||
77 | insertTake' :: (PSQKey k, Ord p) => Int -> k -> v -> p -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v | ||
78 | insertTake' 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. | ||
82 | take :: (PSQKey k, Ord p) => Int -> MinMaxPSQ' k p v -> MinMaxPSQ' k p v | ||
83 | take !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. | ||
88 | takeView :: (PSQKey k, Ord p) => Int -> MinMaxPSQ' k p v -> ( [Binding' k p v], MinMaxPSQ' k p v ) | ||
89 | takeView !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 | |||
98 | lookup' :: (PSQKey k, Ord p) => k -> MinMaxPSQ' k p v -> Maybe (p, v) | ||
99 | lookup' 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 #-} | ||
32 | module 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 | |||
150 | import Prelude | ||
151 | import Control.Applicative | ||
152 | import Control.DeepSeq | ||
153 | import Control.Exception | ||
154 | import Control.Lens | ||
155 | import Control.Monad | ||
156 | import Crypto.Hash | ||
157 | #ifdef VERSION_bencoding | ||
158 | import Data.BEncode as BE | ||
159 | import Data.BEncode.Types as BE | ||
160 | #endif | ||
161 | import Data.Bits | ||
162 | #ifdef VERSION_bits_extras | ||
163 | import Data.Bits.Extras | ||
164 | #endif | ||
165 | import qualified Data.ByteArray as Bytes | ||
166 | import Data.ByteString as BS | ||
167 | import Data.ByteString.Base16 as Base16 | ||
168 | import Data.ByteString.Base32 as Base32 | ||
169 | import Data.ByteString.Base64 as Base64 | ||
170 | import Data.ByteString.Char8 as BC (pack, unpack) | ||
171 | import Data.ByteString.Lazy as BL | ||
172 | import Data.Char | ||
173 | import Data.Convertible | ||
174 | import Data.Default | ||
175 | import Data.Hashable as Hashable | ||
176 | import Data.Int | ||
177 | import Data.List as L | ||
178 | import Data.Map as M | ||
179 | import Data.Maybe | ||
180 | import Data.Serialize as S | ||
181 | import Data.String | ||
182 | import Data.Text as T | ||
183 | import Data.Text.Encoding as T | ||
184 | import Data.Text.Read | ||
185 | import Data.Time.Clock.POSIX | ||
186 | import Data.Typeable | ||
187 | import Network (HostName) | ||
188 | import Network.HTTP.Types.QueryLike | ||
189 | import Network.HTTP.Types.URI | ||
190 | import Network.URI | ||
191 | import Text.ParserCombinators.ReadP as P | ||
192 | import Text.PrettyPrint as PP | ||
193 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
194 | import System.FilePath | ||
195 | import System.Posix.Types | ||
196 | |||
197 | import Network.Address | ||
198 | import 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. | ||
219 | newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString } | ||
220 | deriving (Eq, Ord, Typeable) | ||
221 | |||
222 | infoHashLen :: Int | ||
223 | infoHashLen = 20 | ||
224 | |||
225 | -- | Meaningless placeholder value. | ||
226 | instance Default InfoHash where | ||
227 | def = "0123456789012345678901234567890123456789" | ||
228 | |||
229 | -- | Hash raw bytes. (no encoding) | ||
230 | instance 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) | ||
236 | instance BEncode InfoHash where | ||
237 | toBEncode = toBEncode . getInfoHash | ||
238 | fromBEncode be = InfoHash <$> fromBEncode be | ||
239 | #endif | ||
240 | |||
241 | #if 0 | ||
242 | instance 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) | ||
250 | instance 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) | ||
258 | instance QueryValueLike InfoHash where | ||
259 | toQueryValue (InfoHash ih) = Just ih | ||
260 | {-# INLINE toQueryValue #-} | ||
261 | |||
262 | -- | Convert to base16 encoded string. | ||
263 | instance Show InfoHash where | ||
264 | show (InfoHash ih) = BC.unpack (Base16.encode ih) | ||
265 | |||
266 | -- | Convert to base16 encoded Doc string. | ||
267 | instance Pretty InfoHash where | ||
268 | pPrint = text . show | ||
269 | |||
270 | -- | Read base16 encoded string. | ||
271 | instance 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. | ||
283 | instance 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. | ||
289 | instance 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. | ||
314 | instance IsString InfoHash where | ||
315 | fromString = either (error . prettyConvertError) id . safeConvert . T.pack | ||
316 | |||
317 | ignoreErrorMsg :: Either a b -> Maybe b | ||
318 | ignoreErrorMsg = either (const Nothing) Just | ||
319 | |||
320 | -- | Tries both base16 and base32 while decoding info hash. | ||
321 | -- | ||
322 | -- Use 'safeConvert' for detailed error messages. | ||
323 | -- | ||
324 | textToInfoHash :: Text -> Maybe InfoHash | ||
325 | textToInfoHash = ignoreErrorMsg . safeConvert | ||
326 | |||
327 | -- | Hex encode infohash to text, full length. | ||
328 | longHex :: InfoHash -> Text | ||
329 | longHex = T.decodeUtf8 . Base16.encode . getInfoHash | ||
330 | |||
331 | -- | The same as 'longHex', but only first 7 characters. | ||
332 | shortHex :: InfoHash -> Text | ||
333 | shortHex = T.take 7 . longHex | ||
334 | |||
335 | {----------------------------------------------------------------------- | ||
336 | -- File info | ||
337 | -----------------------------------------------------------------------} | ||
338 | |||
339 | -- | Size of a file in bytes. | ||
340 | type FileSize = FileOffset | ||
341 | |||
342 | #ifdef VERSION_bencoding | ||
343 | deriving instance BEncode FileOffset | ||
344 | #endif | ||
345 | |||
346 | -- | Contain metainfo about one single file. | ||
347 | data 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 | |||
372 | makeLensesFor | ||
373 | [ ("fiLength", "fileLength") | ||
374 | , ("fiMD5Sum", "fileMD5Sum") | ||
375 | , ("fiName" , "filePath" ) | ||
376 | ] | ||
377 | ''FileInfo | ||
378 | |||
379 | instance NFData a => NFData (FileInfo a) where | ||
380 | rnf FileInfo {..} = rnf fiName | ||
381 | {-# INLINE rnf #-} | ||
382 | |||
383 | #ifdef VERSION_bencoding | ||
384 | instance 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 | |||
398 | type Put a = a -> BDict -> BDict | ||
399 | #endif | ||
400 | |||
401 | #ifdef VERSION_bencoding | ||
402 | putFileInfoSingle :: Data.Torrent.Put (FileInfo BS.ByteString) | ||
403 | putFileInfoSingle FileInfo {..} cont = | ||
404 | "length" .=! fiLength | ||
405 | .: "md5sum" .=? fiMD5Sum | ||
406 | .: "name" .=! fiName | ||
407 | .: cont | ||
408 | |||
409 | getFileInfoSingle :: BE.Get (FileInfo BS.ByteString) | ||
410 | getFileInfoSingle = do | ||
411 | FileInfo <$>! "length" | ||
412 | <*>? "md5sum" | ||
413 | <*>! "name" | ||
414 | |||
415 | instance BEncode (FileInfo BS.ByteString) where | ||
416 | toBEncode = toDict . (`putFileInfoSingle` endDict) | ||
417 | {-# INLINE toBEncode #-} | ||
418 | |||
419 | fromBEncode = fromDict getFileInfoSingle | ||
420 | {-# INLINE fromBEncode #-} | ||
421 | #endif | ||
422 | |||
423 | instance 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. | ||
432 | joinFilePath :: FileInfo [BS.ByteString] -> FileInfo BS.ByteString | ||
433 | joinFilePath = 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 | -- | ||
445 | data 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 | |||
459 | makeLensesFor | ||
460 | [ ("liFile" , "singleFile" ) | ||
461 | , ("liFiles" , "multiFile" ) | ||
462 | , ("liDirName", "rootDirName") | ||
463 | ] | ||
464 | ''LayoutInfo | ||
465 | |||
466 | instance NFData LayoutInfo where | ||
467 | rnf SingleFile {..} = () | ||
468 | rnf MultiFile {..} = rnf liFiles | ||
469 | |||
470 | -- | Empty multifile layout. | ||
471 | instance Default LayoutInfo where | ||
472 | def = MultiFile [] "" | ||
473 | |||
474 | #ifdef VERSION_bencoding | ||
475 | getLayoutInfo :: BE.Get LayoutInfo | ||
476 | getLayoutInfo = single <|> multi | ||
477 | where | ||
478 | single = SingleFile <$> getFileInfoSingle | ||
479 | multi = MultiFile <$>! "files" <*>! "name" | ||
480 | |||
481 | putLayoutInfo :: Data.Torrent.Put LayoutInfo | ||
482 | putLayoutInfo SingleFile {..} = putFileInfoSingle liFile | ||
483 | putLayoutInfo MultiFile {..} = \ cont -> | ||
484 | "files" .=! liFiles | ||
485 | .: "name" .=! liDirName | ||
486 | .: cont | ||
487 | |||
488 | instance BEncode LayoutInfo where | ||
489 | toBEncode = toDict . (`putLayoutInfo` endDict) | ||
490 | fromBEncode = fromDict getLayoutInfo | ||
491 | #endif | ||
492 | |||
493 | instance 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. | ||
498 | isSingleFile :: LayoutInfo -> Bool | ||
499 | isSingleFile SingleFile {} = True | ||
500 | isSingleFile _ = False | ||
501 | {-# INLINE isSingleFile #-} | ||
502 | |||
503 | -- | Test if this is multifile torrent. | ||
504 | isMultiFile :: LayoutInfo -> Bool | ||
505 | isMultiFile MultiFile {} = True | ||
506 | isMultiFile _ = False | ||
507 | {-# INLINE isMultiFile #-} | ||
508 | |||
509 | -- | Get name of the torrent based on the root path piece. | ||
510 | suggestedName :: LayoutInfo -> BS.ByteString | ||
511 | suggestedName (SingleFile FileInfo {..}) = fiName | ||
512 | suggestedName MultiFile {..} = liDirName | ||
513 | {-# INLINE suggestedName #-} | ||
514 | |||
515 | -- | Find sum of sizes of the all torrent files. | ||
516 | contentLength :: LayoutInfo -> FileSize | ||
517 | contentLength SingleFile { liFile = FileInfo {..} } = fiLength | ||
518 | contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs) | ||
519 | |||
520 | -- | Get number of all files in torrent. | ||
521 | fileCount :: LayoutInfo -> Int | ||
522 | fileCount SingleFile {..} = 1 | ||
523 | fileCount 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. | ||
527 | blockCount :: Int -> LayoutInfo -> Int | ||
528 | blockCount 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 | -- | ||
537 | type FileLayout a = [(FilePath, a)] | ||
538 | |||
539 | -- | Extract files layout from torrent info with the given root path. | ||
540 | flatLayout | ||
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. | ||
544 | flatLayout prefixPath SingleFile { liFile = FileInfo {..} } | ||
545 | = [(prefixPath </> BC.unpack fiName, fiLength)] | ||
546 | flatLayout 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. | ||
554 | accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize) | ||
555 | accumPositions = 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. | ||
561 | fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset | ||
562 | fileOffset = L.lookup | ||
563 | {-# INLINE fileOffset #-} | ||
564 | |||
565 | ------------------------------------------------------------------------ | ||
566 | |||
567 | -- | Divide and round up. | ||
568 | sizeInBase :: Integral a => a -> Int -> Int | ||
569 | sizeInBase 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. | ||
580 | type 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 | -- | ||
588 | type PieceSize = Int | ||
589 | |||
590 | -- | Number of pieces in torrent or a part of torrent. | ||
591 | type PieceCount = Int | ||
592 | |||
593 | defaultBlockSize :: Int | ||
594 | defaultBlockSize = 16 * 1024 | ||
595 | |||
596 | -- | Optimal number of pieces in torrent. | ||
597 | optimalPieceCount :: PieceCount | ||
598 | optimalPieceCount = 1000 | ||
599 | {-# INLINE optimalPieceCount #-} | ||
600 | |||
601 | -- | Piece size should not be less than this value. | ||
602 | minPieceSize :: Int | ||
603 | minPieceSize = defaultBlockSize * 4 | ||
604 | {-# INLINE minPieceSize #-} | ||
605 | |||
606 | -- | To prevent transfer degradation piece size should not exceed this | ||
607 | -- value. | ||
608 | maxPieceSize :: Int | ||
609 | maxPieceSize = 4 * 1024 * 1024 | ||
610 | {-# INLINE maxPieceSize #-} | ||
611 | |||
612 | toPow2 :: Int -> Int | ||
613 | #ifdef VERSION_bits_extras | ||
614 | toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x) | ||
615 | #else | ||
616 | toPow2 x = bit $ fromIntegral (countLeadingZeros (0 :: Int) - countLeadingZeros x) | ||
617 | #endif | ||
618 | |||
619 | -- | Find the optimal piece size for a given torrent size. | ||
620 | defaultPieceSize :: Int64 -> Int | ||
621 | defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc | ||
622 | where | ||
623 | pc = fromIntegral (x `div` fromIntegral optimalPieceCount) | ||
624 | |||
625 | {----------------------------------------------------------------------- | ||
626 | -- Piece data | ||
627 | -----------------------------------------------------------------------} | ||
628 | |||
629 | type PieceHash = BS.ByteString | ||
630 | |||
631 | hashsize :: Int | ||
632 | hashsize = 20 | ||
633 | {-# INLINE hashsize #-} | ||
634 | |||
635 | -- TODO check if pieceLength is power of 2 | ||
636 | -- | Piece payload should be strict or lazy bytestring. | ||
637 | data 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 | |||
645 | instance NFData a => NFData (Piece a) where | ||
646 | rnf (Piece a b) = rnf a `seq` rnf b | ||
647 | |||
648 | -- | Payload bytes are omitted. | ||
649 | instance Pretty (Piece a) where | ||
650 | pPrint Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex) | ||
651 | |||
652 | -- | Get size of piece in bytes. | ||
653 | pieceSize :: Piece BL.ByteString -> PieceSize | ||
654 | pieceSize Piece {..} = fromIntegral (BL.length pieceData) | ||
655 | |||
656 | -- | Get piece hash. | ||
657 | hashPiece :: Piece BL.ByteString -> PieceHash | ||
658 | hashPiece Piece {..} = Bytes.convert (hashlazy pieceData :: Digest SHA1) | ||
659 | |||
660 | {----------------------------------------------------------------------- | ||
661 | -- Piece control | ||
662 | -----------------------------------------------------------------------} | ||
663 | |||
664 | -- | A flat array of SHA1 hash for each piece. | ||
665 | newtype 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. | ||
673 | instance Default HashList where | ||
674 | def = HashList "" | ||
675 | |||
676 | -- | Part of torrent file used for torrent content validation. | ||
677 | data 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. | ||
686 | makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo | ||
687 | |||
688 | -- | Concatenation of all 20-byte SHA1 hash values. | ||
689 | makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo | ||
690 | |||
691 | instance NFData PieceInfo where | ||
692 | rnf (PieceInfo a (HashList b)) = rnf a `seq` rnf b | ||
693 | |||
694 | instance Default PieceInfo where | ||
695 | def = PieceInfo 1 def | ||
696 | |||
697 | |||
698 | #ifdef VERSION_bencoding | ||
699 | putPieceInfo :: Data.Torrent.Put PieceInfo | ||
700 | putPieceInfo PieceInfo {..} cont = | ||
701 | "piece length" .=! piPieceLength | ||
702 | .: "pieces" .=! piPieceHashes | ||
703 | .: cont | ||
704 | |||
705 | getPieceInfo :: BE.Get PieceInfo | ||
706 | getPieceInfo = do | ||
707 | PieceInfo <$>! "piece length" | ||
708 | <*>! "pieces" | ||
709 | |||
710 | instance BEncode PieceInfo where | ||
711 | toBEncode = toDict . (`putPieceInfo` endDict) | ||
712 | fromBEncode = fromDict getPieceInfo | ||
713 | #endif | ||
714 | |||
715 | -- | Hashes are omitted. | ||
716 | instance Pretty PieceInfo where | ||
717 | pPrint PieceInfo {..} = "Piece size: " <> int piPieceLength | ||
718 | |||
719 | slice :: Int -> Int -> BS.ByteString -> BS.ByteString | ||
720 | slice start len = BS.take len . BS.drop start | ||
721 | {-# INLINE slice #-} | ||
722 | |||
723 | -- | Extract validation hash by specified piece index. | ||
724 | pieceHash :: PieceInfo -> PieceIx -> PieceHash | ||
725 | pieceHash 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. | ||
729 | pieceCount :: PieceInfo -> PieceCount | ||
730 | pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize | ||
731 | |||
732 | -- | Test if this is last piece in torrent content. | ||
733 | isLastPiece :: PieceInfo -> PieceIx -> Bool | ||
734 | isLastPiece ci i = pieceCount ci == succ i | ||
735 | |||
736 | -- | Validate piece with metainfo hash. | ||
737 | checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool | ||
738 | checkPieceLazy 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. | ||
752 | data 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 | |||
770 | makeLensesFor | ||
771 | [ ("idInfoHash" , "infohash" ) | ||
772 | , ("idLayoutInfo", "layoutInfo") | ||
773 | , ("idPieceInfo" , "pieceInfo" ) | ||
774 | , ("idPrivate" , "isPrivate" ) | ||
775 | ] | ||
776 | ''InfoDict | ||
777 | |||
778 | instance NFData InfoDict where | ||
779 | rnf InfoDict {..} = rnf idLayoutInfo | ||
780 | |||
781 | instance Hashable InfoDict where | ||
782 | hashWithSalt = Hashable.hashUsing idInfoHash | ||
783 | {-# INLINE hashWithSalt #-} | ||
784 | |||
785 | -- | Hash lazy bytestring using SHA1 algorithm. | ||
786 | hashLazyIH :: BL.ByteString -> InfoHash | ||
787 | hashLazyIH = 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. | ||
793 | instance Default InfoDict where | ||
794 | def = infoDictionary def def False | ||
795 | |||
796 | -- | Smart constructor: add a info hash to info dictionary. | ||
797 | infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict | ||
798 | infoDictionary li pinfo private = InfoDict ih li pinfo private | ||
799 | where | ||
800 | ih = hashLazyIH $ BE.encode $ InfoDict def li pinfo private | ||
801 | |||
802 | getPrivate :: BE.Get Bool | ||
803 | getPrivate = (Just True ==) <$>? "private" | ||
804 | |||
805 | putPrivate :: Bool -> BDict -> BDict | ||
806 | putPrivate False = id | ||
807 | putPrivate True = \ cont -> "private" .=! True .: cont | ||
808 | |||
809 | instance 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 | |||
824 | ppPrivacy :: Bool -> Doc | ||
825 | ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" | ||
826 | |||
827 | --ppAdditionalInfo :: InfoDict -> Doc | ||
828 | --ppAdditionalInfo layout = PP.empty | ||
829 | |||
830 | instance 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. | ||
842 | data 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 | |||
888 | makeLensesFor | ||
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 | |||
902 | instance NFData Torrent where | ||
903 | rnf Torrent {..} = rnf tInfoDict | ||
904 | |||
905 | #ifdef VERSION_bencoding | ||
906 | -- TODO move to bencoding | ||
907 | instance 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 | ||
919 | instance 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 | ||
925 | instance {-# OVERLAPPING #-} BEncode String where | ||
926 | toBEncode = toBEncode . T.pack | ||
927 | fromBEncode v = T.unpack <$> fromBEncode v | ||
928 | |||
929 | instance 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 | ||
959 | name <:> v = name <> ":" <+> v | ||
960 | |||
961 | (<:>?) :: Doc -> Maybe Doc -> Doc | ||
962 | _ <:>? Nothing = PP.empty | ||
963 | name <:>? (Just d) = name <:> d | ||
964 | |||
965 | instance 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... | ||
990 | instance Default Torrent where | ||
991 | def = nullTorrent def | ||
992 | #endif | ||
993 | |||
994 | -- | A simple torrent contains only required fields. | ||
995 | nullTorrent :: InfoDict -> Torrent | ||
996 | nullTorrent info = Torrent | ||
997 | Nothing Nothing Nothing Nothing Nothing Nothing | ||
998 | info Nothing Nothing Nothing Nothing | ||
999 | |||
1000 | -- | Mime type of torrent files. | ||
1001 | typeTorrent :: BS.ByteString | ||
1002 | typeTorrent = "application/x-bittorrent" | ||
1003 | |||
1004 | -- | Extension usually used for torrent files. | ||
1005 | torrentExt :: String | ||
1006 | torrentExt = "torrent" | ||
1007 | |||
1008 | -- | Test if this path has proper extension. | ||
1009 | isTorrentPath :: FilePath -> Bool | ||
1010 | isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt | ||
1011 | |||
1012 | #ifdef VERSION_bencoding | ||
1013 | -- | Read and decode a .torrent file. | ||
1014 | fromFile :: FilePath -> IO Torrent | ||
1015 | fromFile 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. | ||
1022 | toFile :: FilePath -> Torrent -> IO () | ||
1023 | toFile 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. | ||
1032 | type 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 | -- | ||
1038 | btih :: NamespaceId | ||
1039 | btih = ["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 | -- | ||
1046 | data 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 | |||
1054 | instance 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 | -- | ||
1062 | infohashURN :: InfoHash -> URN | ||
1063 | infohashURN = URN btih . longHex | ||
1064 | |||
1065 | -- | Meaningless placeholder value. | ||
1066 | instance Default URN where | ||
1067 | def = infohashURN def | ||
1068 | |||
1069 | ------------------------------------------------------------------------ | ||
1070 | |||
1071 | -- | Render URN to its text representation. | ||
1072 | renderURN :: URN -> Text | ||
1073 | renderURN URN {..} | ||
1074 | = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString] | ||
1075 | |||
1076 | instance Pretty URN where | ||
1077 | pPrint = text . T.unpack . renderURN | ||
1078 | |||
1079 | instance Show URN where | ||
1080 | showsPrec n = showsPrec n . T.unpack . renderURN | ||
1081 | |||
1082 | instance 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 | |||
1092 | instance 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 | |||
1105 | instance 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 | -- | ||
1113 | parseURN :: Text -> Maybe URN | ||
1114 | parseURN = 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. | ||
1141 | data 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 | |||
1174 | instance QueryValueLike Integer where | ||
1175 | toQueryValue = toQueryValue . show | ||
1176 | |||
1177 | instance QueryValueLike URI where | ||
1178 | toQueryValue = toQueryValue . show | ||
1179 | |||
1180 | instance 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 | |||
1192 | instance QueryValueLike Magnet where | ||
1193 | toQueryValue = toQueryValue . renderMagnet | ||
1194 | |||
1195 | instance 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 | |||
1216 | magnetScheme :: URI | ||
1217 | magnetScheme = URI | ||
1218 | { uriScheme = "magnet:" | ||
1219 | , uriAuthority = Nothing | ||
1220 | , uriPath = "" | ||
1221 | , uriQuery = "" | ||
1222 | , uriFragment = "" | ||
1223 | } | ||
1224 | |||
1225 | isMagnetURI :: URI -> Bool | ||
1226 | isMagnetURI u = u { uriQuery = "" } == magnetScheme | ||
1227 | |||
1228 | -- | Can be used instead of 'parseMagnet'. | ||
1229 | instance 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'. | ||
1235 | instance Convertible Magnet URI where | ||
1236 | safeConvert m = pure $ magnetScheme | ||
1237 | { uriQuery = BC.unpack $ renderQuery True $ toQuery m } | ||
1238 | |||
1239 | instance 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. | ||
1247 | instance 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. | ||
1261 | nullMagnet :: InfoHash -> Magnet | ||
1262 | nullMagnet 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). | ||
1275 | simpleMagnet :: Torrent -> Magnet | ||
1276 | simpleMagnet 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 | -- | ||
1284 | detailedMagnet :: Torrent -> Magnet | ||
1285 | detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce} | ||
1286 | = (simpleMagnet t) | ||
1287 | { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo | ||
1288 | , tracker = tAnnounce | ||
1289 | } | ||
1290 | |||
1291 | ----------------------------------------------------------------------- | ||
1292 | |||
1293 | parseMagnetStr :: String -> Maybe Magnet | ||
1294 | parseMagnetStr = either (const Nothing) Just . safeConvert | ||
1295 | |||
1296 | renderMagnetStr :: Magnet -> String | ||
1297 | renderMagnetStr = show . (convert :: Magnet -> URI) | ||
1298 | |||
1299 | instance Pretty Magnet where | ||
1300 | pPrint = PP.text . renderMagnetStr | ||
1301 | |||
1302 | instance Show Magnet where | ||
1303 | show = renderMagnetStr | ||
1304 | {-# INLINE show #-} | ||
1305 | |||
1306 | instance Read Magnet where | ||
1307 | readsPrec _ xs | ||
1308 | | Just m <- parseMagnetStr mstr = [(m, rest)] | ||
1309 | | otherwise = [] | ||
1310 | where | ||
1311 | (mstr, rest) = L.break (== ' ') xs | ||
1312 | |||
1313 | instance 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 | -- | ||
1321 | parseMagnet :: Text -> Maybe Magnet | ||
1322 | parseMagnet = parseMagnetStr . T.unpack | ||
1323 | {-# INLINE parseMagnet #-} | ||
1324 | |||
1325 | -- | Render magnet link to urlencoded string | ||
1326 | renderMagnet :: Magnet -> Text | ||
1327 | renderMagnet = 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 #-} | ||
4 | module Data.Word64Map where | ||
5 | |||
6 | import Data.Bits | ||
7 | import qualified Data.IntMap as IntMap | ||
8 | ;import Data.IntMap (IntMap) | ||
9 | import Data.Typeable | ||
10 | import 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'. | ||
18 | fitsInInt :: forall proxy word. (Bounded word, Integral word) => proxy word -> Bool | ||
19 | fitsInInt proxy = (original == casted) | ||
20 | where | ||
21 | original = div maxBound 2 :: word | ||
22 | casted = fromIntegral (fromIntegral original :: Int) :: word | ||
23 | |||
24 | newtype Word64Map a = Word64Map (IntMap (IntMap a)) | ||
25 | |||
26 | empty :: Word64Map a | ||
27 | empty = Word64Map IntMap.empty | ||
28 | |||
29 | -- Warning: This function assumes an 'Int' is either 64 or 32 bits. | ||
30 | keyFrom64 :: Word64 -> (# Int,Int #) | ||
31 | keyFrom64 w8 = | ||
32 | if fitsInInt (Proxy :: Proxy Word64) | ||
33 | then (# fromIntegral w8 , 0 #) | ||
34 | else (# fromIntegral (w8 `shiftR` 32), fromIntegral w8 #) | ||
35 | {-# INLINE keyFrom64 #-} | ||
36 | |||
37 | lookup :: Word64 -> Word64Map b -> Maybe b | ||
38 | lookup w8 (Word64Map m) | (# hi,lo #) <- keyFrom64 w8 = do | ||
39 | m' <- IntMap.lookup hi m | ||
40 | IntMap.lookup lo m' | ||
41 | {-# INLINE lookup #-} | ||
42 | |||
43 | insert :: Word64 -> b -> Word64Map b -> Word64Map b | ||
44 | insert 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 | |||
51 | delete :: Word64 -> Word64Map b -> Word64Map b | ||
52 | delete 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 #-} | ||
4 | module Data.Wrapper.PSQ | ||
5 | #if 0 | ||
6 | ( module Data.Wrapper.PSQ , module Data.PSQueue ) where | ||
7 | |||
8 | import Data.PSQueue hiding (foldr, foldl) | ||
9 | import qualified Data.PSQueue as PSQueue | ||
10 | |||
11 | type PSQKey k = (Ord k) | ||
12 | |||
13 | -- | Wrapper over PSQueue-style foldr to implement a psqueues-style interface. | ||
14 | fold' :: (Ord k, Ord p) => (k -> p -> () -> a -> a) -> a -> PSQ k p -> a | ||
15 | fold' 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 | |||
22 | import Data.OrdPSQ as OrdPSQ hiding (insert, map, singleton, minView) | ||
23 | import qualified Data.OrdPSQ as OrdPSQ | ||
24 | |||
25 | import Data.HashPSQ as HashPSQ hiding (insert, map, singleton, minView) | ||
26 | import qualified Data.HashPSQ as Q | ||
27 | import Data.Hashable | ||
28 | |||
29 | type PSQ' k p v = HashPSQ k p v | ||
30 | type PSQ k p = PSQ' k p () | ||
31 | |||
32 | type Binding' k p v = (k,p,v) | ||
33 | type Binding k p = Binding' k p () | ||
34 | |||
35 | type PSQKey k = (Hashable k, Ord k) | ||
36 | |||
37 | pattern (:->) :: k -> p -> Binding k p | ||
38 | pattern 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... | ||
42 | pattern Binding :: k -> v -> p -> Binding' k p v | ||
43 | pattern Binding k v p <- (k,p,v) where Binding k v p = (k,p,v) | ||
44 | |||
45 | key :: (k,p,v) -> k | ||
46 | key (k,p,v) = k | ||
47 | {-# INLINE key #-} | ||
48 | |||
49 | prio :: (k,p,v) -> p | ||
50 | prio (k,p,v) = p | ||
51 | {-# INLINE prio #-} | ||
52 | |||
53 | insert :: (PSQKey k, Ord p) => k -> p -> PSQ k p -> PSQ k p | ||
54 | insert k p q = Q.insert k p () q | ||
55 | {-# INLINE insert #-} | ||
56 | |||
57 | insert' :: (PSQKey k, Ord p) => k -> v -> p -> PSQ' k p v -> PSQ' k p v | ||
58 | insert' k v p q = Q.insert k p v q | ||
59 | {-# INLINE insert' #-} | ||
60 | |||
61 | insertWith :: (PSQKey k, Ord p) => (p -> p -> p) -> k -> p -> PSQ k p -> PSQ k p | ||
62 | insertWith 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 | |||
68 | singleton :: (PSQKey k, Ord p) => k -> p -> PSQ k p | ||
69 | singleton k p = Q.singleton k p () | ||
70 | {-# INLINE singleton #-} | ||
71 | |||
72 | singleton' :: (PSQKey k, Ord p) => k -> v -> p -> PSQ' k p v | ||
73 | singleton' k v p = Q.singleton k p v | ||
74 | {-# INLINE singleton' #-} | ||
75 | |||
76 | |||
77 | minView :: (PSQKey k, Ord p) => PSQ' k p v -> Maybe (Binding' k p v, PSQ' k p v) | ||
78 | minView 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 #-} | ||
4 | module Data.Wrapper.PSQInt | ||
5 | #if 0 | ||
6 | ( module Data.Wrapper.PSQInt , module Data.PSQueue ) where | ||
7 | |||
8 | import Data.PSQueue hiding (foldr, foldl, PSQ) | ||
9 | import qualified Data.PSQueue as PSQueue | ||
10 | |||
11 | type PSQ p = PSQueue.PSQ Int p | ||
12 | |||
13 | -- | Wrapper over PSQueue-style foldr to implement a psqueues-style interface. | ||
14 | fold' :: (Ord p) => (Int -> p -> () -> a -> a) -> a -> PSQ p -> a | ||
15 | fold' 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 | |||
27 | import Data.Wrapper.PSQ (Binding, pattern (:->), key, prio) | ||
28 | |||
29 | import Data.IntPSQ as IntPSQ hiding (insert, map, singleton, minView) | ||
30 | import qualified Data.IntPSQ as Q | ||
31 | |||
32 | type PSQ p = IntPSQ p () | ||
33 | |||
34 | type PSQKey = () | ||
35 | |||
36 | insert :: (Ord p) => Int -> p -> PSQ p -> PSQ p | ||
37 | insert k p q = Q.insert k p () q | ||
38 | {-# INLINE insert #-} | ||
39 | |||
40 | insertWith :: (Ord p) => (p -> p -> p) -> Int -> p -> PSQ p -> PSQ p | ||
41 | insertWith 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 | |||
47 | singleton :: (Ord p) => Int -> p -> PSQ p | ||
48 | singleton k p = Q.singleton k p () | ||
49 | {-# INLINE singleton #-} | ||
50 | |||
51 | minView :: (Ord p) => PSQ p -> Maybe (Binding Int p, PSQ p) | ||
52 | minView 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 #-} | ||
27 | module 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 | |||
89 | import Control.Applicative | ||
90 | import Control.Monad | ||
91 | import Control.Exception (onException) | ||
92 | #ifdef VERSION_bencoding | ||
93 | import Data.BEncode as BE | ||
94 | import Data.BEncode.BDict (BKey) | ||
95 | #endif | ||
96 | import Data.Bits | ||
97 | import qualified Data.ByteString as BS | ||
98 | import qualified Data.ByteString.Internal as BS | ||
99 | import Data.ByteString.Char8 as BC | ||
100 | import Data.ByteString.Char8 as BS8 | ||
101 | import qualified Data.ByteString.Lazy as BL | ||
102 | import qualified Data.ByteString.Lazy.Builder as BS | ||
103 | import Data.Char | ||
104 | import Data.Convertible | ||
105 | import Data.Default | ||
106 | import Data.IP | ||
107 | import Data.List as L | ||
108 | import Data.List.Split as L | ||
109 | import Data.Maybe (fromMaybe, catMaybes, mapMaybe) | ||
110 | import Data.Monoid | ||
111 | import Data.Hashable | ||
112 | import Data.Ord | ||
113 | import Data.Serialize as S | ||
114 | import Data.String | ||
115 | import Data.Time | ||
116 | import Data.Typeable | ||
117 | import Data.Version | ||
118 | import Data.Word | ||
119 | import qualified Text.ParserCombinators.ReadP as RP | ||
120 | import Text.Read (readMaybe) | ||
121 | import Network.HTTP.Types.QueryLike | ||
122 | import Network.Socket | ||
123 | import Text.PrettyPrint as PP hiding ((<>)) | ||
124 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
125 | #if !MIN_VERSION_time(1,5,0) | ||
126 | import System.Locale (defaultTimeLocale) | ||
127 | #endif | ||
128 | import System.Entropy | ||
129 | import System.IO (stderr) | ||
130 | |||
131 | -- import Paths_bittorrent (version) | ||
132 | |||
133 | instance Pretty UTCTime where | ||
134 | pPrint = PP.text . show | ||
135 | |||
136 | setPort :: PortNumber -> SockAddr -> SockAddr | ||
137 | setPort port (SockAddrInet _ h ) = SockAddrInet port h | ||
138 | setPort port (SockAddrInet6 _ f h s) = SockAddrInet6 port f h s | ||
139 | setPort _ addr = addr | ||
140 | {-# INLINE setPort #-} | ||
141 | |||
142 | -- | Obtains the port associated with a socket address | ||
143 | -- if one is associated with it. | ||
144 | sockAddrPort :: SockAddr -> Maybe PortNumber | ||
145 | sockAddrPort (SockAddrInet p _ ) = Just p | ||
146 | sockAddrPort (SockAddrInet6 p _ _ _) = Just p | ||
147 | sockAddrPort _ = Nothing | ||
148 | {-# INLINE sockAddrPort #-} | ||
149 | |||
150 | class (Eq a, Serialize a, Typeable a, Hashable a, Pretty a) | ||
151 | => Address a where | ||
152 | toSockAddr :: a -> SockAddr | ||
153 | fromSockAddr :: SockAddr -> Maybe a | ||
154 | |||
155 | fromAddr :: (Address a, Address b) => a -> Maybe b | ||
156 | fromAddr = fromSockAddr . toSockAddr | ||
157 | |||
158 | -- | Note that port is zeroed. | ||
159 | instance 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. | ||
165 | instance 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. | ||
171 | instance 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 | |||
178 | data NodeAddr a = NodeAddr | ||
179 | { nodeHost :: !a | ||
180 | , nodePort :: {-# UNPACK #-} !PortNumber | ||
181 | } deriving (Eq, Ord, Typeable, Functor, Foldable, Traversable) | ||
182 | |||
183 | instance Show a => Show (NodeAddr a) where | ||
184 | showsPrec i NodeAddr {..} | ||
185 | = showsPrec i nodeHost <> showString ":" <> showsPrec i nodePort | ||
186 | |||
187 | instance 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@ | ||
195 | instance Default (NodeAddr IPv4) where | ||
196 | def = "127.0.0.1:6882" | ||
197 | |||
198 | -- | KRPC compatible encoding. | ||
199 | instance 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 | -- | ||
209 | instance 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 | |||
218 | instance Hashable a => Hashable (NodeAddr a) where | ||
219 | hashWithSalt s NodeAddr {..} = hashWithSalt s (nodeHost, nodePort) | ||
220 | {-# INLINE hashWithSalt #-} | ||
221 | |||
222 | instance Pretty ip => Pretty (NodeAddr ip) where | ||
223 | pPrint NodeAddr {..} = pPrint nodeHost <> ":" <> pPrint nodePort | ||
224 | |||
225 | |||
226 | |||
227 | instance 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. | ||
246 | newtype PeerId = PeerId { getPeerId :: ByteString } | ||
247 | deriving ( Show, Eq, Ord, Typeable | ||
248 | #ifdef VERSION_bencoding | ||
249 | , BEncode | ||
250 | #endif | ||
251 | ) | ||
252 | |||
253 | peerIdLen :: Int | ||
254 | peerIdLen = 20 | ||
255 | |||
256 | -- | For testing purposes only. | ||
257 | instance Default PeerId where | ||
258 | def = azureusStyle defaultClientId defaultVersionNumber "" | ||
259 | |||
260 | instance Hashable PeerId where | ||
261 | hashWithSalt = hashUsing getPeerId | ||
262 | {-# INLINE hashWithSalt #-} | ||
263 | |||
264 | instance Serialize PeerId where | ||
265 | put = putByteString . getPeerId | ||
266 | get = PeerId <$> getBytes peerIdLen | ||
267 | |||
268 | instance QueryValueLike PeerId where | ||
269 | toQueryValue (PeerId pid) = Just pid | ||
270 | {-# INLINE toQueryValue #-} | ||
271 | |||
272 | instance 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 | |||
279 | instance Pretty PeerId where | ||
280 | pPrint = text . BC.unpack . getPeerId | ||
281 | |||
282 | instance 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 | -- | ||
299 | byteStringPadded :: ByteString -- ^ bytestring to be padded. | ||
300 | -> Int -- ^ size of result builder. | ||
301 | -> Char -- ^ character used for padding. | ||
302 | -> BS.Builder | ||
303 | byteStringPadded 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 | -- | ||
321 | azureusStyle :: 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. | ||
325 | azureusStyle 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 | -- | ||
342 | shadowStyle :: Char -- ^ Client ID. | ||
343 | -> ByteString -- ^ Version number. | ||
344 | -> ByteString -- ^ Random number. | ||
345 | -> PeerId -- ^ Shadow style encoded peer ID. | ||
346 | shadowStyle 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. | ||
353 | defaultClientId :: ByteString | ||
354 | defaultClientId = "HS" | ||
355 | |||
356 | -- | Gives exactly 4 bytes long version number for any version of the | ||
357 | -- package. Version is taken from .cabal file. | ||
358 | defaultVersionNumber :: ByteString | ||
359 | defaultVersionNumber = 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 | -- | ||
378 | timestamp :: IO ByteString | ||
379 | timestamp = (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'. | ||
386 | entropy :: IO ByteString | ||
387 | entropy = 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 | -- | ||
400 | genPeerId :: IO PeerId | ||
401 | genPeerId = 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 | ||
417 | instance 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 | |||
431 | class IPAddress i where | ||
432 | toHostAddr :: i -> Either HostAddress HostAddress6 | ||
433 | |||
434 | instance IPAddress IPv4 where | ||
435 | toHostAddr = Left . toHostAddress | ||
436 | {-# INLINE toHostAddr #-} | ||
437 | |||
438 | instance IPAddress IPv6 where | ||
439 | toHostAddr = Right . toHostAddress6 | ||
440 | {-# INLINE toHostAddr #-} | ||
441 | |||
442 | instance IPAddress IP where | ||
443 | toHostAddr (IPv4 ip) = toHostAddr ip | ||
444 | toHostAddr (IPv6 ip) = toHostAddr ip | ||
445 | {-# INLINE toHostAddr #-} | ||
446 | |||
447 | deriving instance Typeable IP | ||
448 | deriving instance Typeable IPv4 | ||
449 | deriving instance Typeable IPv6 | ||
450 | |||
451 | #ifdef VERSION_bencoding | ||
452 | ipToBEncode :: Show i => i -> BValue | ||
453 | ipToBEncode ip = BString $ BS8.pack $ show ip | ||
454 | {-# INLINE ipToBEncode #-} | ||
455 | |||
456 | ipFromBEncode :: Read a => BValue -> BE.Result a | ||
457 | ipFromBEncode (BString (BS8.unpack -> ipStr)) | ||
458 | | Just ip <- readMaybe (ipStr) = pure ip | ||
459 | | otherwise = decodingError $ "IP: " ++ ipStr | ||
460 | ipFromBEncode _ = decodingError $ "IP: addr should be a bstring" | ||
461 | |||
462 | instance BEncode IP where | ||
463 | toBEncode = ipToBEncode | ||
464 | {-# INLINE toBEncode #-} | ||
465 | fromBEncode = ipFromBEncode | ||
466 | {-# INLINE fromBEncode #-} | ||
467 | |||
468 | instance BEncode IPv4 where | ||
469 | toBEncode = ipToBEncode | ||
470 | {-# INLINE toBEncode #-} | ||
471 | fromBEncode = ipFromBEncode | ||
472 | {-# INLINE fromBEncode #-} | ||
473 | |||
474 | instance 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. | ||
483 | data 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 | ||
495 | peer_ip_key, peer_id_key, peer_port_key :: BKey | ||
496 | peer_ip_key = "ip" | ||
497 | peer_id_key = "peer id" | ||
498 | peer_port_key = "port" | ||
499 | |||
500 | -- | The tracker's 'announce response' compatible encoding. | ||
501 | instance 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 | -- | ||
523 | instance 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@ | ||
530 | instance Default PeerAddr where | ||
531 | def = "127.0.0.1:6881" | ||
532 | |||
533 | -- | Example: | ||
534 | -- | ||
535 | -- @peerPort \"127.0.0.1:6881\" == 6881@ | ||
536 | -- | ||
537 | instance 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 | |||
547 | instance 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 | |||
555 | readsIPv6_port :: String -> [((IPv6, PortNumber), String)] | ||
556 | readsIPv6_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 | ||
564 | instance 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 | |||
571 | instance Hashable PeerAddr where | ||
572 | hashWithSalt s PeerAddr {..} = | ||
573 | s `hashWithSalt` peerId `hashWithSalt` peerHost `hashWithSalt` peerPort | ||
574 | |||
575 | -- | Ports typically reserved for bittorrent P2P listener. | ||
576 | defaultPorts :: [PortNumber] | ||
577 | defaultPorts = [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 | |||
587 | peerSockAddr :: PeerAddr -> SockAddr | ||
588 | peerSockAddr = snd . _peerSockAddr | ||
589 | |||
590 | -- | Create a socket connected to the address specified in a peerAddr | ||
591 | peerSocket :: SocketType -> PeerAddr -> IO Socket | ||
592 | peerSocket 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. | ||
616 | testIdBit :: NodeId -> Word -> Bool | ||
617 | testIdBit (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 | |||
624 | testIdBit :: FiniteBits bs => bs -> Word -> Bool | ||
625 | testIdBit 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) | ||
633 | genBucketSample :: ( FiniteBits nid | ||
634 | , Serialize nid | ||
635 | ) => nid -> (Int,Word8,Word8) -> IO nid | ||
636 | genBucketSample 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. | ||
640 | genBucketSample' :: forall m dht nid. | ||
641 | ( Applicative m | ||
642 | , FiniteBits nid | ||
643 | , Serialize nid | ||
644 | ) => | ||
645 | (Int -> m ByteString) -> nid -> (Int,Word8,Word8) -> m nid | ||
646 | genBucketSample' 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. | ||
676 | bucketRange :: Int -> Bool -> (Int, Word8, Word8) | ||
677 | bucketRange 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. | ||
687 | instance 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 | |||
695 | instance Hashable PortNumber where | ||
696 | hashWithSalt s = hashWithSalt s . fromEnum | ||
697 | {-# INLINE hashWithSalt #-} | ||
698 | |||
699 | instance Pretty PortNumber where | ||
700 | pPrint = PP.int . fromEnum | ||
701 | {-# INLINE pPrint #-} | ||
702 | |||
703 | instance Serialize PortNumber where | ||
704 | get = fromIntegral <$> getWord16be | ||
705 | {-# INLINE get #-} | ||
706 | put = putWord16be . fromIntegral | ||
707 | {-# INLINE put #-} | ||
708 | |||
709 | instance Pretty IPv4 where | ||
710 | pPrint = PP.text . show | ||
711 | {-# INLINE pPrint #-} | ||
712 | |||
713 | instance Pretty IPv6 where | ||
714 | pPrint = PP.text . show | ||
715 | {-# INLINE pPrint #-} | ||
716 | |||
717 | instance 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 | ||
725 | instance 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 | |||
736 | instance Serialize IPv4 where | ||
737 | put = putWord32host . toHostAddress | ||
738 | get = fromHostAddress <$> getWord32host | ||
739 | |||
740 | instance Serialize IPv6 where | ||
741 | put ip = put $ toHostAddress6 ip | ||
742 | get = fromHostAddress6 <$> get | ||
743 | |||
744 | |||
745 | instance Hashable IPv4 where | ||
746 | hashWithSalt = hashUsing toHostAddress | ||
747 | {-# INLINE hashWithSalt #-} | ||
748 | |||
749 | instance Hashable IPv6 where | ||
750 | hashWithSalt s a = hashWithSalt s (toHostAddress6 a) | ||
751 | |||
752 | instance 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 | ||
782 | version :: Version | ||
783 | version = 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 | -- | ||
790 | data 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 | |||
868 | parseSoftware :: ByteString -> Software | ||
869 | parseSoftware = 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 | ||
935 | instance Default Software where | ||
936 | def = IUnknown | ||
937 | {-# INLINE def #-} | ||
938 | |||
939 | -- | Example: @\"BitLet\" == 'IBitLet'@ | ||
940 | instance 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\"@ | ||
949 | instance Pretty Software where | ||
950 | pPrint = text . L.tail . show | ||
951 | |||
952 | -- | Just the '0' version. | ||
953 | instance 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 | -- | ||
960 | instance 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 | |||
967 | instance 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. | ||
972 | data Fingerprint = Fingerprint Software Version | ||
973 | deriving (Show, Eq, Ord) | ||
974 | |||
975 | -- | Unrecognized client implementation. | ||
976 | instance Default Fingerprint where | ||
977 | def = Fingerprint def def | ||
978 | {-# INLINE def #-} | ||
979 | |||
980 | -- | Example: @\"BitComet-1.2\" == ClientInfo IBitComet (Version [1, 2] [])@ | ||
981 | instance 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 | |||
988 | instance 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 | -- | ||
995 | libFingerprint :: Fingerprint | ||
996 | libFingerprint = Fingerprint IlibHSbittorrent version | ||
997 | |||
998 | -- | HTTP user agent of this (the bittorrent library) package. Can be | ||
999 | -- used in HTTP tracker requests. | ||
1000 | libUserAgent :: String | ||
1001 | libUserAgent = 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 | -- | ||
1099 | fingerprint :: PeerId -> Fingerprint | ||
1100 | fingerprint 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. | ||
1172 | getBindAddress :: String -> Bool -> IO SockAddr | ||
1173 | getBindAddress 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. | ||
1197 | is4mapped :: IPv6 -> Bool | ||
1198 | is4mapped ip | ||
1199 | | [0,0,0,0,0,0xffff,_,_] <- fromIPv6 ip | ||
1200 | = True | ||
1201 | | otherwise = False | ||
1202 | |||
1203 | un4map :: IPv6 -> Maybe IPv4 | ||
1204 | un4map 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 | |||
1211 | ipFamily :: IP -> WantIP | ||
1212 | ipFamily ip = case ip of | ||
1213 | IPv4 _ -> Want_IP4 | ||
1214 | IPv6 a | is4mapped a -> Want_IP4 | ||
1215 | | otherwise -> Want_IP6 | ||
1216 | |||
1217 | either4or6 :: SockAddr -> Either SockAddr SockAddr | ||
1218 | either4or6 a4@(SockAddrInet port addr) = Left a4 | ||
1219 | either4or6 a6@(SockAddrInet6 port _ addr _) | ||
1220 | | Just ip4 <- (fromSockAddr a6 >>= un4map) = Left (setPort port $ toSockAddr ip4) | ||
1221 | | otherwise = Right a6 | ||
1222 | |||
1223 | data 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 #-} | ||
2 | module 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 | |||
11 | import Control.Applicative | ||
12 | import Data.Default | ||
13 | import Data.List as L | ||
14 | import Data.Maybe | ||
15 | import Data.HashMap.Strict as HM | ||
16 | import Data.Serialize | ||
17 | import Data.Wrapper.PSQ as PSQ | ||
18 | import Data.Time.Clock.POSIX | ||
19 | import Data.ByteString (ByteString) | ||
20 | import Data.Word | ||
21 | import Network.Socket (SockAddr(..)) | ||
22 | |||
23 | import Data.Torrent | ||
24 | import 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. | ||
120 | newtype PeerStore = PeerStore (HashMap InfoHash SwarmData) | ||
121 | |||
122 | type Timestamp = POSIXTime | ||
123 | |||
124 | data 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. | ||
133 | newtype SerializeAddress a = SerializeAddress { unserializeAddress :: a } | ||
134 | |||
135 | instance 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 | |||
152 | instance 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 | |||
168 | knownSwarms :: PeerStore -> [ (InfoHash, Int, Maybe ByteString) ] | ||
169 | knownSwarms (PeerStore m) = L.map (\(ih,SwarmData q n) -> (ih,PSQ.size q,n)) $ HM.toList m | ||
170 | |||
171 | swarmSingleton :: PeerAddr -> SwarmData | ||
172 | swarmSingleton a = SwarmData | ||
173 | { peers = PSQ.singleton a 0 | ||
174 | , name = Nothing } | ||
175 | |||
176 | swarmInsert :: SwarmData -> SwarmData -> SwarmData | ||
177 | swarmInsert 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 | |||
184 | isSwarmOccupied :: SwarmData -> Bool | ||
185 | isSwarmOccupied SwarmData{..} = not $ PSQ.null peers | ||
186 | |||
187 | -- | Empty store. | ||
188 | instance Default (PeerStore) where | ||
189 | def = PeerStore HM.empty | ||
190 | {-# INLINE def #-} | ||
191 | |||
192 | -- | Monoid under union operation. | ||
193 | instance 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. | ||
203 | instance 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. | ||
208 | lookup :: InfoHash -> PeerStore -> [PeerAddr] | ||
209 | lookup ih (PeerStore m) = maybe [] (PSQ.keys . peers) $ HM.lookup ih m | ||
210 | |||
211 | batchSize :: Int | ||
212 | batchSize = 64 | ||
213 | |||
214 | -- | Used in 'get_peers' DHT queries. | ||
215 | freshPeers :: InfoHash -> Timestamp -> PeerStore -> ([PeerAddr], PeerStore) | ||
216 | freshPeers 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 | |||
226 | incomp :: (x -> Maybe (r,x)) -> x -> Maybe ((r,x),x) | ||
227 | incomp !f !x = do | ||
228 | (result,x') <- f x | ||
229 | pure $! ( (result,x'), x' ) | ||
230 | |||
231 | -- | Used in 'announce_peer' DHT queries. | ||
232 | insertPeer :: InfoHash -> Maybe ByteString -> PeerAddr -> PeerStore -> PeerStore | ||
233 | insertPeer !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 @@ | |||
1 | References | ||
2 | ========== | ||
3 | |||
4 | Some 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 #-} | ||
21 | module 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 | |||
47 | import Control.Arrow | ||
48 | import Control.Monad.State | ||
49 | #ifdef VERSION_bencoding | ||
50 | import Data.BEncode (BEncode) | ||
51 | #endif | ||
52 | import Data.ByteString as BS | ||
53 | import Data.ByteString.Char8 as B8 | ||
54 | import Data.ByteString.Lazy as BL | ||
55 | import Data.ByteString.Lazy.Builder as BS | ||
56 | import qualified Data.ByteString.Base16 as Base16 | ||
57 | import Data.Default | ||
58 | import Data.List as L | ||
59 | import Data.Hashable | ||
60 | import Data.String | ||
61 | import Data.Time | ||
62 | import System.Random | ||
63 | import Control.Concurrent.STM | ||
64 | import Network.Address | ||
65 | |||
66 | -- TODO use ShortByteString | ||
67 | |||
68 | -- | An opaque value. | ||
69 | newtype Token = Token BS.ByteString | ||
70 | deriving ( Eq, IsString | ||
71 | #ifdef VERSION_bencoding | ||
72 | , BEncode | ||
73 | #endif | ||
74 | ) | ||
75 | |||
76 | instance Show Token where | ||
77 | show (Token bs) = B8.unpack $ Base16.encode bs | ||
78 | |||
79 | instance Read Token where | ||
80 | readsPrec i s = pure $ (Token *** B8.unpack) $ Base16.decode (B8.pack s) | ||
81 | |||
82 | -- | Meaningless token, for testing purposes only. | ||
83 | instance 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. | ||
90 | toPaddedByteString :: Int -> Token -> BS.ByteString | ||
91 | toPaddedByteString n (Token bs) = BS.append (BS.replicate (n - BS.length bs) 0x20) bs | ||
92 | |||
93 | fromPaddedByteString :: Int -> BS.ByteString -> Token | ||
94 | fromPaddedByteString 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. | ||
99 | type Secret = Int | ||
100 | |||
101 | -- The BitTorrent implementation uses the SHA1 hash of the IP address | ||
102 | -- concatenated onto a secret, we use hashable instead. | ||
103 | makeToken :: Hashable a => a -> Secret -> Token | ||
104 | makeToken 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. | ||
110 | data 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. | ||
121 | tokens :: Int -> TokenMap | ||
122 | tokens 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. | ||
131 | lookup :: Hashable a => a -> TokenMap -> Token | ||
132 | lookup 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. | ||
139 | member :: Hashable a => a -> Token -> TokenMap -> Bool | ||
140 | member 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. | ||
145 | defaultUpdateInterval :: NominalDiffTime | ||
146 | defaultUpdateInterval = 5 * 60 | ||
147 | |||
148 | -- | Update current tokens. | ||
149 | update :: TokenMap -> TokenMap | ||
150 | update TokenMap {..} = TokenMap | ||
151 | { prevSecret = curSecret | ||
152 | , curSecret = newSecret | ||
153 | , generator = newGen | ||
154 | } | ||
155 | where | ||
156 | (newSecret, newGen) = next generator | ||
157 | |||
158 | data SessionTokens = SessionTokens | ||
159 | { tokenMap :: !TokenMap | ||
160 | , lastUpdate :: !UTCTime | ||
161 | , maxInterval :: !NominalDiffTime | ||
162 | } | ||
163 | |||
164 | nullSessionTokens :: IO SessionTokens | ||
165 | nullSessionTokens = SessionTokens | ||
166 | <$> (tokens <$> randomIO) | ||
167 | <*> getCurrentTime | ||
168 | <*> pure defaultUpdateInterval | ||
169 | |||
170 | -- TODO invalidate *twice* if needed | ||
171 | invalidateTokens :: UTCTime -> SessionTokens -> SessionTokens | ||
172 | invalidateTokens 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 | |||
184 | tryUpdateSecret :: TVar SessionTokens -> IO () | ||
185 | tryUpdateSecret toks = do | ||
186 | curTime <- getCurrentTime | ||
187 | atomically $ modifyTVar' toks (invalidateTokens curTime) | ||
188 | |||
189 | grantToken :: Hashable addr => TVar SessionTokens -> addr -> IO Token | ||
190 | grantToken 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. | ||
197 | checkToken :: Hashable addr => TVar SessionTokens -> addr -> Token -> IO Bool | ||
198 | checkToken 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 #-} | ||
13 | module Network.BitTorrent.MainlineDHT where | ||
14 | |||
15 | import Control.Applicative | ||
16 | import Control.Arrow | ||
17 | import Control.Concurrent.STM | ||
18 | import Control.Monad | ||
19 | import Crypto.Random | ||
20 | import Data.BEncode as BE | ||
21 | import qualified Data.BEncode.BDict as BE | ||
22 | ;import Data.BEncode.BDict (BKey) | ||
23 | import Data.BEncode.Pretty | ||
24 | import Data.BEncode.Types (BDict) | ||
25 | import Data.Bits | ||
26 | import Data.Bits.ByteString | ||
27 | import Data.Bool | ||
28 | import qualified Data.ByteArray as BA | ||
29 | ;import Data.ByteArray (ByteArrayAccess) | ||
30 | import qualified Data.ByteString as B | ||
31 | ;import Data.ByteString (ByteString) | ||
32 | import qualified Data.ByteString.Base16 as Base16 | ||
33 | import qualified Data.ByteString.Char8 as C8 | ||
34 | import Data.ByteString.Lazy (toStrict) | ||
35 | import qualified Data.ByteString.Lazy.Char8 as L8 | ||
36 | import Data.Char | ||
37 | import Data.Coerce | ||
38 | import Data.Data | ||
39 | import Data.Default | ||
40 | import Data.Digest.CRC32C | ||
41 | import Data.Function (fix) | ||
42 | import Data.Hashable | ||
43 | import Data.IP | ||
44 | import Data.List | ||
45 | import Data.Maybe | ||
46 | import Data.Monoid | ||
47 | import Data.Ord | ||
48 | import qualified Data.Serialize as S | ||
49 | import Data.Set (Set) | ||
50 | import Data.Time.Clock.POSIX (POSIXTime) | ||
51 | import Data.Torrent | ||
52 | import Data.Typeable | ||
53 | import Data.Word | ||
54 | import qualified Data.Wrapper.PSQInt as Int | ||
55 | import Debug.Trace | ||
56 | import Network.BitTorrent.MainlineDHT.Symbols | ||
57 | import Network.Kademlia | ||
58 | import Network.Kademlia.Bootstrap | ||
59 | import Network.Address (Address, fromAddr, fromSockAddr, | ||
60 | setPort, sockAddrPort, testIdBit, | ||
61 | toSockAddr, genBucketSample', WantIP(..), | ||
62 | un4map,either4or6,ipFamily) | ||
63 | import Network.BitTorrent.DHT.ContactInfo as Peers | ||
64 | import Network.Kademlia.Search (Search (..)) | ||
65 | import Network.BitTorrent.DHT.Token as Token | ||
66 | import qualified Network.Kademlia.Routing as R | ||
67 | ;import Network.Kademlia.Routing (Timestamp, getTimestamp) | ||
68 | import Network.QueryResponse | ||
69 | import Network.Socket | ||
70 | import System.IO | ||
71 | import System.IO.Error | ||
72 | import System.IO.Unsafe (unsafeInterleaveIO) | ||
73 | import qualified Text.ParserCombinators.ReadP as RP | ||
74 | #ifdef THREAD_DEBUG | ||
75 | import Control.Concurrent.Lifted.Instrument | ||
76 | #else | ||
77 | import Control.Concurrent.Lifted | ||
78 | import GHC.Conc (labelThread) | ||
79 | #endif | ||
80 | import Control.Exception (SomeException (..), handle) | ||
81 | import qualified Data.Aeson as JSON | ||
82 | ;import Data.Aeson (FromJSON, ToJSON, (.=)) | ||
83 | import Text.Read | ||
84 | import System.Global6 | ||
85 | import Control.TriadCommittee | ||
86 | |||
87 | newtype NodeId = NodeId ByteString | ||
88 | deriving (Eq,Ord,ByteArrayAccess, Bits, Hashable) | ||
89 | |||
90 | instance 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 | |||
99 | instance Show NodeId where | ||
100 | show (NodeId bs) = C8.unpack $ Base16.encode bs | ||
101 | |||
102 | instance S.Serialize NodeId where | ||
103 | get = NodeId <$> S.getBytes 20 | ||
104 | put (NodeId bs) = S.putByteString bs | ||
105 | |||
106 | instance FiniteBits NodeId where | ||
107 | finiteBitSize _ = 160 | ||
108 | |||
109 | instance 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 | |||
116 | zeroID :: NodeId | ||
117 | zeroID = NodeId $ B.replicate 20 0 | ||
118 | |||
119 | data NodeInfo = NodeInfo | ||
120 | { nodeId :: NodeId | ||
121 | , nodeIP :: IP | ||
122 | , nodePort :: PortNumber | ||
123 | } | ||
124 | deriving (Eq,Ord) | ||
125 | |||
126 | instance 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 | ] | ||
143 | instance 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 | |||
155 | hexdigit :: Char -> Bool | ||
156 | hexdigit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'f') || ( 'A' <= c && c <= 'F') | ||
157 | |||
158 | instance 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. | ||
189 | instance Hashable NodeInfo where | ||
190 | hashWithSalt s ni = hashWithSalt s (nodeIP ni , nodePort ni) | ||
191 | {-# INLINE hashWithSalt #-} | ||
192 | |||
193 | |||
194 | instance 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 | |||
212 | getNodeInfo4 :: S.Get NodeInfo | ||
213 | getNodeInfo4 = NodeInfo <$> (NodeId <$> S.getBytes 20) | ||
214 | <*> (IPv4 <$> S.get) | ||
215 | <*> S.get | ||
216 | |||
217 | putNodeInfo4 :: NodeInfo -> S.Put | ||
218 | putNodeInfo4 (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 | |||
225 | getNodeInfo6 :: S.Get NodeInfo | ||
226 | getNodeInfo6 = NodeInfo <$> (NodeId <$> S.getBytes 20) | ||
227 | <*> (IPv6 <$> S.get) | ||
228 | <*> S.get | ||
229 | |||
230 | putNodeInfo6 :: NodeInfo -> S.Put | ||
231 | putNodeInfo6 (NodeInfo (NodeId nid) (IPv6 ip) port) | ||
232 | = S.putByteString nid >> S.put ip >> S.put port | ||
233 | putNodeInfo6 _ = 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. | ||
239 | nodeAddr :: NodeInfo -> SockAddr | ||
240 | nodeAddr (NodeInfo _ ip port) = | ||
241 | case ip of | ||
242 | IPv4 ip4 -> setPort port $ toSockAddr (ipv4ToIPv6 ip4) | ||
243 | IPv6 ip6 -> setPort port $ toSockAddr ip6 | ||
244 | |||
245 | nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo | ||
246 | nodeInfo 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. | ||
252 | data 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> | ||
268 | instance 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 | |||
281 | instance BEncode ErrorCode where | ||
282 | toBEncode = toBEncode . fromEnum | ||
283 | {-# INLINE toBEncode #-} | ||
284 | fromBEncode b = toEnum <$> fromBEncode b | ||
285 | {-# INLINE fromBEncode #-} | ||
286 | |||
287 | data 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 | |||
292 | newtype TransactionId = TransactionId ByteString | ||
293 | deriving (Eq, Ord, Show, BEncode) | ||
294 | |||
295 | newtype Method = Method ByteString | ||
296 | deriving (Eq, Ord, Show, BEncode) | ||
297 | |||
298 | data 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 | |||
309 | showBE :: BValue -> String | ||
310 | showBE bval = L8.unpack (showBEncode bval) | ||
311 | |||
312 | instance 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 | |||
325 | decodeMessage :: BValue -> Either String (Message BValue) | ||
326 | decodeMessage = 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 | |||
359 | encodeMessage :: Message BValue -> BValue | ||
360 | encodeMessage (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. | ||
364 | encodeMessage (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 | |||
370 | encodeAddr :: SockAddr -> ByteString | ||
371 | encodeAddr = 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 | |||
380 | decodeAddr :: ByteString -> Either String SockAddr | ||
381 | decodeAddr 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 | |||
388 | genericArgs :: BEncode a => a -> Bool -> BDict | ||
389 | genericArgs nodeid ro = | ||
390 | "id" .=! nodeid | ||
391 | .: "ro" .=? bool Nothing (Just (1 :: Int)) ro | ||
392 | .: endDict | ||
393 | |||
394 | encodeError :: BEncode a => a -> Error -> BValue | ||
395 | encodeError tid (Error ecode emsg) = encodeAny tid "e" (ecode,emsg) id | ||
396 | |||
397 | encodeResponse :: (BEncode tid, BEncode vals) => | ||
398 | tid -> vals -> Maybe SockAddr -> BValue | ||
399 | encodeResponse tid rvals rip = | ||
400 | encodeAny tid "r" rvals ("ip" .=? (BString . encodeAddr <$> rip) .:) | ||
401 | |||
402 | encodeQuery :: (BEncode args, BEncode tid, BEncode method) => | ||
403 | tid -> method -> args -> BValue | ||
404 | encodeQuery tid qmeth qargs = encodeAny tid "q" qmeth ("a" .=! qargs .:) | ||
405 | |||
406 | encodeAny :: | ||
407 | (BEncode tid, BEncode a) => | ||
408 | tid -> BKey -> a -> (BDict -> BDict) -> BValue | ||
409 | encodeAny tid key val aux = toDict $ | ||
410 | aux $ key .=! val | ||
411 | .: "t" .=! tid | ||
412 | .: "y" .=! key | ||
413 | .: endDict | ||
414 | |||
415 | |||
416 | showPacket :: ([L8.ByteString] -> [L8.ByteString]) -> SockAddr -> L8.ByteString -> ByteString -> String | ||
417 | showPacket 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. | ||
426 | addVerbosity :: Transport err SockAddr ByteString -> Transport err SockAddr ByteString | ||
427 | addVerbosity 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 | |||
438 | showParseError :: ByteString -> SockAddr -> String -> String | ||
439 | showParseError bs addr err = showPacket (L8.pack err :) addr " --> " bs | ||
440 | |||
441 | parsePacket :: ByteString -> SockAddr -> Either String (Message BValue, NodeInfo) | ||
442 | parsePacket 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 | |||
449 | encodePacket :: Message BValue -> NodeInfo -> (ByteString, SockAddr) | ||
450 | encodePacket msg ni = ( toStrict $ BE.encode msg | ||
451 | , nodeAddr ni ) | ||
452 | |||
453 | classify :: Message BValue -> MessageClass String Method TransactionId NodeInfo (Message BValue) | ||
454 | classify (Q { msgID = tid, qryMethod = meth }) = IsQuery meth tid | ||
455 | classify (R { msgID = tid }) = IsResponse tid | ||
456 | |||
457 | encodeResponsePayload :: BEncode a => TransactionId -> NodeInfo -> NodeInfo -> a -> Message BValue | ||
458 | encodeResponsePayload tid self dest b = R (nodeId self) tid (Right $ BE.toBEncode b) (Just $ nodeAddr dest) | ||
459 | |||
460 | encodeQueryPayload :: BEncode a => | ||
461 | Method -> Bool -> TransactionId -> NodeInfo -> NodeInfo -> a -> Message BValue | ||
462 | encodeQueryPayload meth isReadonly tid self dest b = Q (nodeId self) tid (BE.toBEncode b) meth isReadonly | ||
463 | |||
464 | errorPayload :: TransactionId -> NodeInfo -> NodeInfo -> Error -> Message a | ||
465 | errorPayload tid self dest e = R (nodeId self) tid (Left e) (Just $ nodeAddr dest) | ||
466 | |||
467 | decodePayload :: BEncode a => Message BValue -> Either String a | ||
468 | decodePayload msg = BE.fromBEncode $ qryPayload msg | ||
469 | |||
470 | type Handler = MethodHandler String TransactionId NodeInfo (Message BValue) | ||
471 | |||
472 | handler :: ( BEncode a | ||
473 | , BEncode b | ||
474 | ) => | ||
475 | (NodeInfo -> a -> IO b) -> Maybe Handler | ||
476 | handler f = Just $ MethodHandler decodePayload encodeResponsePayload f | ||
477 | |||
478 | |||
479 | handlerE :: ( BEncode a | ||
480 | , BEncode b | ||
481 | ) => | ||
482 | (NodeInfo -> a -> IO (Either Error b)) -> Maybe Handler | ||
483 | handlerE 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 | |||
488 | type AnnounceSet = Set (InfoHash, PortNumber) | ||
489 | |||
490 | data 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 | |||
496 | newSwarmsDatabase :: IO SwarmsDatabase | ||
497 | newSwarmsDatabase = do | ||
498 | toks <- nullSessionTokens | ||
499 | atomically | ||
500 | $ SwarmsDatabase <$> newTVar def | ||
501 | <*> newTVar toks | ||
502 | <*> newTVar def | ||
503 | |||
504 | data 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 | |||
512 | sched4 :: Routing -> TVar (Int.PSQ POSIXTime) | ||
513 | sched4 Routing { refresher4 = BucketRefresher { refreshQueue } } = refreshQueue | ||
514 | |||
515 | sched6 :: Routing -> TVar (Int.PSQ POSIXTime) | ||
516 | sched6 Routing { refresher6 = BucketRefresher { refreshQueue } } = refreshQueue | ||
517 | |||
518 | routing4 :: Routing -> TVar (R.BucketList NodeInfo) | ||
519 | routing4 Routing { refresher4 = BucketRefresher { refreshBuckets } } = refreshBuckets | ||
520 | |||
521 | routing6 :: Routing -> TVar (R.BucketList NodeInfo) | ||
522 | routing6 Routing { refresher6 = BucketRefresher { refreshBuckets } } = refreshBuckets | ||
523 | |||
524 | traced :: Show tid => TableMethods t tid -> TableMethods t tid | ||
525 | traced (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 | |||
531 | type MainlineClient = Client String Method TransactionId NodeInfo (Message BValue) | ||
532 | |||
533 | -- | Like 'nodeInfo' but falls back to 'iNADDR_ANY' for nodeIP' and 'nodePort'. | ||
534 | mkNodeInfo :: NodeId -> SockAddr -> NodeInfo | ||
535 | mkNodeInfo nid addr = NodeInfo | ||
536 | { nodeId = nid | ||
537 | , nodeIP = fromMaybe (toEnum 0) $ fromSockAddr addr | ||
538 | , nodePort = fromMaybe 0 $ sockAddrPort addr | ||
539 | } | ||
540 | |||
541 | newClient :: SwarmsDatabase -> SockAddr | ||
542 | -> IO ( MainlineClient | ||
543 | , Routing | ||
544 | , [NodeInfo] -> [NodeInfo] -> IO () | ||
545 | , [NodeInfo] -> [NodeInfo] -> IO () | ||
546 | ) | ||
547 | newClient 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 | ||
673 | bep42 :: SockAddr -> NodeId -> Maybe NodeId | ||
674 | bep42 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 | |||
695 | defaultHandler :: ByteString -> Handler | ||
696 | defaultHandler meth = MethodHandler decodePayload errorPayload returnError | ||
697 | where | ||
698 | returnError :: NodeInfo -> BValue -> IO Error | ||
699 | returnError _ _ = return $ Error MethodUnknown ("Unknown method " <> meth) | ||
700 | |||
701 | mainlineKademlia :: MainlineClient | ||
702 | -> TriadCommittee NodeId SockAddr | ||
703 | -> BucketRefresher NodeId NodeInfo | ||
704 | -> Kademlia NodeId NodeInfo | ||
705 | mainlineKademlia 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 | |||
722 | mainlineSpace :: R.KademliaSpace NodeId NodeInfo | ||
723 | mainlineSpace = R.KademliaSpace | ||
724 | { R.kademliaLocation = nodeId | ||
725 | , R.kademliaTestBit = testIdBit | ||
726 | , R.kademliaXor = xor | ||
727 | , R.kademliaSample = genBucketSample' | ||
728 | } | ||
729 | |||
730 | transitionCommittee :: TriadCommittee NodeId SockAddr -> RoutingTransition NodeInfo -> STM (IO ()) | ||
731 | transitionCommittee committee (RoutingTransition ni Stranger) = do | ||
732 | delVote committee (nodeId ni) | ||
733 | return $ do | ||
734 | hPutStrLn stderr $ "delVote "++show (nodeId ni) | ||
735 | transitionCommittee committee _ = return $ return () | ||
736 | |||
737 | updateRouting :: MainlineClient -> Routing -> NodeInfo -> Message BValue -> IO () | ||
738 | updateRouting 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 | |||
754 | data Ping = Ping deriving Show | ||
755 | |||
756 | -- Pong is the same as Ping. | ||
757 | type Pong = Ping | ||
758 | pattern Pong = Ping | ||
759 | |||
760 | instance BEncode Ping where | ||
761 | toBEncode Ping = toDict endDict | ||
762 | fromBEncode _ = pure Ping | ||
763 | |||
764 | wantList :: WantIP -> [ByteString] | ||
765 | wantList Want_IP4 = ["ip4"] | ||
766 | wantList Want_IP6 = ["ip6"] | ||
767 | wantList Want_Both = ["ip4","ip6"] | ||
768 | |||
769 | instance 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 | |||
780 | data FindNode = FindNode NodeId (Maybe WantIP) | ||
781 | |||
782 | instance 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 | |||
789 | data NodeFound = NodeFound | ||
790 | { nodes4 :: [NodeInfo] | ||
791 | , nodes6 :: [NodeInfo] | ||
792 | } | ||
793 | |||
794 | instance 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 | |||
810 | binary :: S.Get a -> BKey -> BE.Get [a] | ||
811 | binary 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 | |||
816 | pingH :: NodeInfo -> Ping -> IO Pong | ||
817 | pingH _ Ping = return Pong | ||
818 | |||
819 | prefer4or6 :: NodeInfo -> Maybe WantIP -> WantIP | ||
820 | prefer4or6 addr iptyp = fromMaybe (ipFamily $ nodeIP addr) iptyp | ||
821 | |||
822 | findNodeH :: Routing -> NodeInfo -> FindNode -> IO NodeFound | ||
823 | findNodeH 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 | |||
841 | data GetPeers = GetPeers InfoHash (Maybe WantIP) | ||
842 | |||
843 | instance 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 | |||
851 | data 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 | |||
864 | nodeIsIPv6 :: NodeInfo -> Bool | ||
865 | nodeIsIPv6 (NodeInfo _ (IPv6 _) _) = True | ||
866 | nodeIsIPv6 _ = False | ||
867 | |||
868 | instance 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 | |||
898 | getPeersH :: Routing -> SwarmsDatabase -> NodeInfo -> GetPeers -> IO GotPeers | ||
899 | getPeersH 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. | ||
918 | data 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 | |||
942 | mkAnnounce :: PortNumber -> InfoHash -> Token -> Announce | ||
943 | mkAnnounce portnum info token = Announce | ||
944 | { topic = info | ||
945 | , port = portnum | ||
946 | , sessionToken = token | ||
947 | , announcedName = Nothing | ||
948 | , impliedPort = False | ||
949 | } | ||
950 | |||
951 | |||
952 | instance 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. | ||
979 | data Announced = Announced | ||
980 | deriving (Show, Eq, Typeable) | ||
981 | |||
982 | instance BEncode Announced where | ||
983 | toBEncode _ = toBEncode Ping | ||
984 | fromBEncode _ = pure Announced | ||
985 | |||
986 | announceH :: SwarmsDatabase -> NodeInfo -> Announce -> IO (Either Error Announced) | ||
987 | announceH (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 | |||
1007 | isReadonlyClient :: MainlineClient -> Bool | ||
1008 | isReadonlyClient client = False -- TODO | ||
1009 | |||
1010 | mainlineSend :: ( BEncode a | ||
1011 | , BEncode a2 | ||
1012 | ) => Method | ||
1013 | -> (a2 -> b) | ||
1014 | -> (t -> a) | ||
1015 | -> MainlineClient | ||
1016 | -> t | ||
1017 | -> NodeInfo | ||
1018 | -> IO (Maybe b) | ||
1019 | mainlineSend 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 | |||
1036 | ping :: MainlineClient -> NodeInfo -> IO Bool | ||
1037 | ping client addr = | ||
1038 | fromMaybe False | ||
1039 | <$> mainlineSend (Method "ping") (\Pong -> True) (const Ping) client () addr | ||
1040 | |||
1041 | -- searchQuery :: ni -> IO (Maybe [ni], [r], tok)) | ||
1042 | getNodes :: MainlineClient -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[NodeInfo],Maybe ())) | ||
1043 | getNodes = mainlineSend (Method "find_node") unwrapNodes $ flip FindNode (Just Want_Both) | ||
1044 | |||
1045 | unwrapNodes :: NodeFound -> ([NodeInfo], [NodeInfo], Maybe ()) | ||
1046 | unwrapNodes (NodeFound ns4 ns6) = (ns4++ns6, ns4++ns6, Just ()) | ||
1047 | |||
1048 | getPeers :: MainlineClient -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[PeerAddr],Maybe Token)) | ||
1049 | getPeers = mainlineSend (Method "get_peers") unwrapPeers $ flip GetPeers (Just Want_Both) . coerce | ||
1050 | |||
1051 | unwrapPeers :: GotPeers -> ([NodeInfo], [PeerAddr], Maybe Token) | ||
1052 | unwrapPeers (GotPeers ps (NodeFound ns4 ns6) tok) = (ns4++ns6, ps, Just tok) | ||
1053 | |||
1054 | mainlineSearch :: (NodeId -> NodeInfo -> IO (Maybe ([NodeInfo], [r], Maybe tok))) | ||
1055 | -> Search NodeId (IP, PortNumber) tok NodeInfo r | ||
1056 | mainlineSearch qry = Search | ||
1057 | { searchSpace = mainlineSpace | ||
1058 | , searchNodeAddress = nodeIP &&& nodePort | ||
1059 | , searchQuery = qry | ||
1060 | } | ||
1061 | |||
1062 | nodeSearch :: MainlineClient -> Search NodeId (IP, PortNumber) () NodeInfo NodeInfo | ||
1063 | nodeSearch client = mainlineSearch (getNodes client) | ||
1064 | |||
1065 | peerSearch :: MainlineClient -> Search NodeId (IP, PortNumber) Token NodeInfo PeerAddr | ||
1066 | peerSearch client = mainlineSearch (getPeers client) | ||
1067 | |||
1068 | -- | List of bootstrap nodes maintained by different bittorrent | ||
1069 | -- software authors. | ||
1070 | bootstrapNodes :: WantIP -> IO [NodeInfo] | ||
1071 | bootstrapNodes 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. | ||
1091 | resolve :: WantIP -> String -> IO (Either IOError SockAddr) | ||
1092 | resolve 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 | |||
1109 | announce :: MainlineClient -> Announce -> NodeInfo -> IO (Maybe Announced) | ||
1110 | announce 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 #-} | ||
2 | module Network.BitTorrent.MainlineDHT.Symbols where | ||
3 | |||
4 | import Data.BEncode.BDict | ||
5 | |||
6 | peer_ip_key = "ip" :: BKey | ||
7 | peer_id_key = "peer id" :: BKey | ||
8 | peer_port_key = "port" :: BKey | ||
9 | msg_type_key = "msg_type" :: BKey | ||
10 | piece_key = "piece" :: BKey | ||
11 | total_size_key = "total_size" :: BKey | ||
12 | node_id_key = "id" :: BKey | ||
13 | read_only_key = "ro" :: BKey | ||
14 | want_key = "want" :: BKey | ||
15 | target_key = "target" :: BKey | ||
16 | nodes_key = "nodes" :: BKey | ||
17 | nodes6_key = "nodes6" :: BKey | ||
18 | info_hash_key = "info_hash" :: BKey | ||
19 | peers_key = "values" :: BKey | ||
20 | token_key = "token" :: BKey | ||
21 | name_key = "name" :: BKey | ||
22 | port_key = "port" :: BKey | ||
23 | implied_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 #-} | ||
7 | module Network.Kademlia where | ||
8 | |||
9 | import Data.Function | ||
10 | import Data.Maybe | ||
11 | import qualified Data.Set as Set | ||
12 | import Data.Time.Clock (getCurrentTime) | ||
13 | import Data.Time.Clock.POSIX (getPOSIXTime, utcTimeToPOSIXSeconds) | ||
14 | import Network.Kademlia.Routing as R | ||
15 | #ifdef THREAD_DEBUG | ||
16 | import Control.Concurrent.Lifted.Instrument | ||
17 | #else | ||
18 | import Control.Concurrent.Lifted | ||
19 | import GHC.Conc (labelThread) | ||
20 | #endif | ||
21 | import Control.Concurrent.STM | ||
22 | import Control.Monad | ||
23 | import Data.Bits | ||
24 | import Data.Hashable | ||
25 | import Data.IP | ||
26 | import Data.Monoid | ||
27 | import Data.Serialize (Serialize) | ||
28 | import Data.Time.Clock.POSIX (POSIXTime) | ||
29 | import qualified Data.Wrapper.PSQInt as Int | ||
30 | ;import Data.Wrapper.PSQInt (pattern (:->)) | ||
31 | import Network.Address (bucketRange,genBucketSample) | ||
32 | import Network.Kademlia.Search | ||
33 | import System.Entropy | ||
34 | import System.Timeout | ||
35 | import Text.PrettyPrint as PP hiding (($$), (<>)) | ||
36 | import Text.PrettyPrint.HughesPJClass hiding (($$), (<>)) | ||
37 | import System.IO | ||
38 | import Control.Concurrent.Tasks | ||
39 | |||
40 | -- | The status of a given node with respect to a given routint table. | ||
41 | data 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. | ||
48 | data RoutingTransition ni = RoutingTransition | ||
49 | { transitioningNode :: ni | ||
50 | , transitionedTo :: !RoutingStatus | ||
51 | } | ||
52 | deriving (Eq,Ord,Show,Read) | ||
53 | |||
54 | data 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 | |||
75 | quietInsertions :: InsertionReporter ni | ||
76 | quietInsertions = InsertionReporter | ||
77 | { reportArrival = \_ _ _ -> return () | ||
78 | , reportPingResult = \_ _ _ -> return () | ||
79 | } | ||
80 | |||
81 | contramapIR :: (t -> ni) -> InsertionReporter ni -> InsertionReporter t | ||
82 | contramapIR 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. | ||
88 | data 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 | |||
112 | vanillaIO :: TVar (BucketList ni) -> (ni -> IO Bool) -> TableStateIO ni | ||
113 | vanillaIO 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. | ||
122 | data 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. | ||
131 | transition :: (ni,Maybe (t,ni)) -> [RoutingTransition ni] | ||
132 | transition (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 | ||
141 | accepted :: (t,ni) -> RoutingTransition ni | ||
142 | accepted (_,y) = RoutingTransition y Accepted | ||
143 | |||
144 | |||
145 | insertNode :: Kademlia nid ni -> ni -> IO () | ||
146 | insertNode (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 #-} | ||
14 | module Network.Kademlia.Bootstrap where | ||
15 | |||
16 | import Data.Function | ||
17 | import Data.Maybe | ||
18 | import qualified Data.Set as Set | ||
19 | import Data.Time.Clock (getCurrentTime) | ||
20 | import Data.Time.Clock.POSIX (getPOSIXTime, utcTimeToPOSIXSeconds) | ||
21 | import Network.Kademlia.Routing as R | ||
22 | #ifdef THREAD_DEBUG | ||
23 | import Control.Concurrent.Lifted.Instrument | ||
24 | #else | ||
25 | import Control.Concurrent.Lifted | ||
26 | import GHC.Conc (labelThread) | ||
27 | #endif | ||
28 | import Control.Concurrent.STM | ||
29 | import Control.Monad | ||
30 | import Data.Bits | ||
31 | import Data.Hashable | ||
32 | import Data.IP | ||
33 | import Data.Monoid | ||
34 | import Data.Serialize (Serialize) | ||
35 | import Data.Time.Clock.POSIX (POSIXTime) | ||
36 | import Data.Ord | ||
37 | import System.Entropy | ||
38 | import System.Timeout | ||
39 | import Text.PrettyPrint as PP hiding (($$), (<>)) | ||
40 | import Text.PrettyPrint.HughesPJClass hiding (($$), (<>)) | ||
41 | import System.IO | ||
42 | |||
43 | import qualified Data.Wrapper.PSQInt as Int | ||
44 | ;import Data.Wrapper.PSQInt (pattern (:->)) | ||
45 | import Network.Address (bucketRange,genBucketSample) | ||
46 | import Network.Kademlia.Search | ||
47 | import Control.Concurrent.Tasks | ||
48 | import Network.Kademlia | ||
49 | |||
50 | type SensibleNodeId nid ni = | ||
51 | ( Show nid | ||
52 | , Ord nid | ||
53 | , Ord ni | ||
54 | , Hashable nid | ||
55 | , Hashable ni ) | ||
56 | |||
57 | data 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 | |||
84 | newBucketRefresher :: ( 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) | ||
90 | newBucketRefresher 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'. | ||
112 | updateRefresherIO :: Ord addr | ||
113 | => Search nid addr tok ni ni | ||
114 | -> (ni -> IO Bool) | ||
115 | -> BucketRefresher nid ni -> BucketRefresher nid ni | ||
116 | updateRefresherIO 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. | ||
128 | forkPollForRefresh :: SensibleNodeId nid ni => BucketRefresher nid ni -> IO ThreadId | ||
129 | forkPollForRefresh 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. | ||
168 | checkBucketFull :: 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 | ||
175 | checkBucketFull 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. | ||
194 | onFinishedRefresh :: BucketRefresher nid ni -> Int -> POSIXTime -> STM (IO ()) | ||
195 | onFinishedRefresh 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 | |||
235 | refreshBucket :: (Show nid, Ord ni, Ord nid, Hashable nid, Hashable ni) => | ||
236 | BucketRefresher nid ni -> Int -> IO Int | ||
237 | refreshBucket 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 | |||
270 | refreshLastBucket :: SensibleNodeId nid ni => BucketRefresher nid ni -> IO () | ||
271 | refreshLastBucket 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 | |||
280 | restartBootstrap :: (Hashable ni, Hashable nid, Ord ni, Ord nid, Show nid) => | ||
281 | BucketRefresher nid ni -> STM (IO ()) | ||
282 | restartBootstrap 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 | |||
291 | bootstrap :: (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 () | ||
296 | bootstrap 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 | |||
329 | effectiveRefreshInterval :: BucketRefresher nid ni -> Int -> STM POSIXTime | ||
330 | effectiveRefreshInterval 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? | ||
401 | touchBucket :: SensibleNodeId nid ni | ||
402 | => BucketRefresher nid ni | ||
403 | -> RoutingTransition ni -- ^ What happened to the bucket? | ||
404 | -> STM (IO ()) | ||
405 | touchBucket 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 #-} | ||
30 | module 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 | |||
73 | import Control.Applicative as A | ||
74 | import Control.Arrow | ||
75 | import Control.Monad | ||
76 | import Data.Function | ||
77 | import Data.Functor.Contravariant | ||
78 | import Data.Functor.Identity | ||
79 | import Data.List as L hiding (insert) | ||
80 | import Data.Maybe | ||
81 | import Data.Monoid | ||
82 | import Data.Wrapper.PSQ as PSQ | ||
83 | import Data.Serialize as S hiding (Result, Done) | ||
84 | import qualified Data.Sequence as Seq | ||
85 | import Data.Time | ||
86 | import Data.Time.Clock.POSIX | ||
87 | import Data.Word | ||
88 | import GHC.Generics | ||
89 | import Text.PrettyPrint as PP hiding ((<>)) | ||
90 | import Text.PrettyPrint.HughesPJClass (pPrint,Pretty) | ||
91 | import qualified Data.ByteString as BS | ||
92 | import Data.Bits | ||
93 | import Data.Ord | ||
94 | import Data.Reflection | ||
95 | import Network.Address | ||
96 | import Data.Typeable | ||
97 | import Data.Coerce | ||
98 | import 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 | -- | ||
116 | type Timestamp = POSIXTime | ||
117 | |||
118 | getTimestamp :: IO Timestamp | ||
119 | getTimestamp = 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. | ||
137 | type NodeEntry ni = Binding ni Timestamp | ||
138 | |||
139 | |||
140 | -- | Maximum number of 'NodeInfo's stored in a bucket. Most clients | ||
141 | -- use this value. | ||
142 | defaultBucketSize :: Int | ||
143 | defaultBucketSize = 8 | ||
144 | |||
145 | data 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 | {- | ||
152 | fromQ :: Functor m => | ||
153 | ( a -> b ) | ||
154 | -> ( b -> a ) | ||
155 | -> QueueMethods m elem a | ||
156 | -> QueueMethods m elem b | ||
157 | fromQ 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 | |||
164 | seqQ :: QueueMethods Identity ni (Seq.Seq ni) | ||
165 | seqQ = 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 | |||
173 | type BucketQueue ni = Seq.Seq ni | ||
174 | |||
175 | bucketQ :: QueueMethods Identity ni (BucketQueue ni) | ||
176 | bucketQ = seqQ | ||
177 | |||
178 | |||
179 | data Compare a = Compare (a -> a -> Ordering) (Int -> a -> Int) | ||
180 | |||
181 | contramapC :: (b -> a) -> Compare a -> Compare b | ||
182 | contramapC f (Compare cmp hsh) = Compare (\a b -> cmp (f a) (f b)) | ||
183 | (\s x -> hsh s (f x)) | ||
184 | |||
185 | newtype Ordered' s a = Ordered a | ||
186 | deriving (Show) | ||
187 | |||
188 | -- | Hack to avoid UndecidableInstances | ||
189 | newtype Shrink a = Shrink a | ||
190 | deriving (Show) | ||
191 | |||
192 | type Ordered s a = Ordered' s (Shrink a) | ||
193 | |||
194 | instance Reifies s (Compare a) => Eq (Ordered' s (Shrink a)) where | ||
195 | a == b = (compare a b == EQ) | ||
196 | |||
197 | instance 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 | |||
201 | instance 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. | ||
211 | data 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 | ||
219 | deriving instance Show ni => Show (Bucket s ni) | ||
220 | #endif | ||
221 | |||
222 | bucketCompare :: forall p ni s. Reifies s (Compare ni) => p (Bucket s ni) -> Compare ni | ||
223 | bucketCompare _ = reflect (Proxy :: Proxy s) | ||
224 | |||
225 | mapBucket :: ( Reifies s (Compare a) | ||
226 | , Reifies t (Compare ni) | ||
227 | ) => (a -> ni) -> Bucket s a -> Bucket t ni | ||
228 | mapBucket 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 | {- | ||
236 | getGenericNode :: ( Serialize (NodeId) | ||
237 | , Serialize ip | ||
238 | , Serialize u | ||
239 | ) => Get (NodeInfo) | ||
240 | getGenericNode = do | ||
241 | nid <- get | ||
242 | naddr <- get | ||
243 | u <- get | ||
244 | return NodeInfo | ||
245 | { nodeId = nid | ||
246 | , nodeAddr = naddr | ||
247 | , nodeAnnotation = u | ||
248 | } | ||
249 | |||
250 | putGenericNode :: ( Serialize (NodeId) | ||
251 | , Serialize ip | ||
252 | , Serialize u | ||
253 | ) => NodeInfo -> Put | ||
254 | putGenericNode (NodeInfo nid naddr u) = do | ||
255 | put nid | ||
256 | put naddr | ||
257 | put u | ||
258 | |||
259 | instance (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 | |||
266 | psqFromPairList :: (Ord p, PSQKey k) => [(k, p)] -> PSQ k p | ||
267 | psqFromPairList xs = PSQ.fromList $ map (\(a,b) -> a :-> b) xs | ||
268 | |||
269 | psqToPairList :: ( PSQKey t, Ord t1 ) => PSQ t t1 -> [(t, t1)] | ||
270 | psqToPairList psq = map (\(a :-> b) -> (a,b)) $ PSQ.toList psq | ||
271 | |||
272 | -- | Update interval, in seconds. | ||
273 | delta :: NominalDiffTime | ||
274 | delta = 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. | ||
279 | updateBucketForInbound :: ( Coercible t1 t | ||
280 | , Alternative f | ||
281 | , Reifies s (Compare t1) | ||
282 | ) => NominalDiffTime -> t1 -> Bucket s t1 -> f ([t], Bucket s t1) | ||
283 | updateBucketForInbound 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 | |||
326 | updateBucketForPingResult :: (Applicative f, Reifies s (Compare a)) => | ||
327 | a -> Bool -> Bucket s a -> f ([(a, Maybe (Timestamp, a))], Bucket s a) | ||
328 | updateBucketForPingResult 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 | |||
357 | updateStamps :: PSQKey ni => Timestamp -> [ni] -> PSQ ni Timestamp -> PSQ ni Timestamp | ||
358 | updateStamps curTime stales nodes = foldl' (\q n -> PSQ.insert n curTime q) nodes stales | ||
359 | |||
360 | type BitIx = Word | ||
361 | |||
362 | partitionQ :: Monad f => QueueMethods f elem b -> (elem -> Bool) -> b -> f (b, b) | ||
363 | partitionQ 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 | |||
380 | split :: -- ( 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) | ||
384 | split 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 | |||
397 | defaultBucketCount :: Int | ||
398 | defaultBucketCount = 20 | ||
399 | |||
400 | defaultMaxBucketCount :: Word | ||
401 | defaultMaxBucketCount = 24 | ||
402 | |||
403 | data Info ni nid = Info | ||
404 | { myBuckets :: BucketList ni | ||
405 | , myNodeId :: nid | ||
406 | , myAddress :: SockAddr | ||
407 | } | ||
408 | deriving Generic | ||
409 | |||
410 | deriving instance (Eq ni, Eq nid) => Eq (Info ni nid) | ||
411 | deriving 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 | -- | ||
431 | data 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 | |||
437 | mapTable :: (b -> t) -> (t -> b) -> BucketList t -> BucketList b | ||
438 | mapTable 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 | |||
447 | instance (Eq ni) => Eq (BucketList ni) where | ||
448 | (==) = (==) `on` Network.Kademlia.Routing.toList | ||
449 | |||
450 | #if 0 | ||
451 | |||
452 | instance Serialize NominalDiffTime where | ||
453 | put = putWord32be . fromIntegral . fromEnum | ||
454 | get = (toEnum . fromIntegral) <$> getWord32be | ||
455 | |||
456 | #endif | ||
457 | |||
458 | #if CAN_SHOW_BUCKET | ||
459 | deriving instance (Show ni) => Show (BucketList ni) | ||
460 | #else | ||
461 | instance 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. | ||
475 | instance (Eq ip, Serialize ip, Ord (NodeId), Serialize (NodeId), Serialize u) => Serialize (BucketList) | ||
476 | |||
477 | #endif | ||
478 | |||
479 | -- | Shape of the table. | ||
480 | instance 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. | ||
493 | nullTable :: (ni -> ni -> Ordering) -> (Int -> ni -> Int) -> ni -> Int -> BucketList ni | ||
494 | nullTable 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'. | ||
507 | null :: BucketList -> Bool | ||
508 | null (Tip _ _ b) = PSQ.null $ bktNodes b | ||
509 | null _ = False | ||
510 | |||
511 | -- | Test if table have maximum number of nodes. No more nodes can be | ||
512 | -- 'insert'ed, except old ones becomes bad. | ||
513 | full :: BucketList -> Bool | ||
514 | full (Tip _ n _) = n == 0 | ||
515 | full (Zero t b) = PSQ.size (bktNodes b) == defaultBucketSize && full t | ||
516 | full (One b t) = PSQ.size (bktNodes b) == defaultBucketSize && full t | ||
517 | |||
518 | -- | Get the /spine/ node id. | ||
519 | thisId :: BucketList -> NodeId | ||
520 | thisId (Tip nid _ _) = nid | ||
521 | thisId (Zero table _) = thisId table | ||
522 | thisId (One _ table) = thisId table | ||
523 | |||
524 | -- | Number of nodes in a bucket or a table. | ||
525 | type 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. | ||
531 | shape :: BucketList ni -> [Int] | ||
532 | shape (BucketList _ tbl) = map (PSQ.size . bktNodes) tbl | ||
533 | |||
534 | #if 0 | ||
535 | |||
536 | -- | Get number of nodes in the table. | ||
537 | size :: BucketList -> NodeCount | ||
538 | size = L.sum . shape | ||
539 | |||
540 | -- | Get number of buckets in the table. | ||
541 | depth :: BucketList -> BucketCount | ||
542 | depth = L.length . shape | ||
543 | |||
544 | #endif | ||
545 | |||
546 | lookupBucket :: 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 | ||
550 | lookupBucket 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 | |||
560 | bucketNumber :: forall ni nid. | ||
561 | KademliaSpace nid ni -> nid -> BucketList ni -> Int | ||
562 | bucketNumber 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 | |||
573 | compatibleNodeId :: forall ni nid. | ||
574 | ( Serialize nid, FiniteBits nid) => | ||
575 | (ni -> nid) -> BucketList ni -> IO nid | ||
576 | compatibleNodeId 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 | |||
583 | tablePrefix :: (ni -> Word -> Bool) -> BucketList ni -> [Word8] | ||
584 | tablePrefix 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 | |||
592 | tableBits :: (ni -> Word -> Bool) -> BucketList ni -> [Bool] | ||
593 | tableBits testbit (BucketList self bkts) = | ||
594 | zipWith const (map (testbit self) [0..]) | ||
595 | bkts | ||
596 | |||
597 | selfNode :: BucketList ni -> ni | ||
598 | selfNode (BucketList self _) = self | ||
599 | |||
600 | chunksOf :: Int -> [e] -> [[e]] | ||
601 | chunksOf 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 | |||
606 | build :: ((a -> [a] -> [a]) -> [a] -> [a]) -> [a] | ||
607 | build g = g (:) [] | ||
608 | |||
609 | |||
610 | |||
611 | -- | Count of closest nodes in find_node reply. | ||
612 | type K = Int | ||
613 | |||
614 | -- | Default 'K' is equal to 'defaultBucketSize'. | ||
615 | defaultK :: K | ||
616 | defaultK = 8 | ||
617 | |||
618 | #if 0 | ||
619 | class TableKey dht k where | ||
620 | toNodeId :: k -> NodeId | ||
621 | |||
622 | instance 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. | ||
629 | newtype NodeDistance nodeid = NodeDistance nodeid | ||
630 | deriving (Eq, Ord) | ||
631 | |||
632 | -- | distance(A,B) = |A xor B| Smaller values are closer. | ||
633 | distance :: Bits nid => nid -> nid -> NodeDistance nid | ||
634 | distance a b = NodeDistance $ xor a b | ||
635 | |||
636 | -- | Order by closeness: nearest nodes first. | ||
637 | rank :: ( Ord nid | ||
638 | ) => KademliaSpace nid ni -> nid -> [ni] -> [ni] | ||
639 | rank 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. | ||
644 | kclosest :: ( -- FiniteBits nid | ||
645 | Ord nid | ||
646 | ) => | ||
647 | KademliaSpace nid ni -> Int -> nid -> BucketList ni -> [ni] | ||
648 | kclosest 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 | |||
662 | splitTip :: -- ( Eq ip , Ord (NodeId) , FiniteBits (NodeId)) => | ||
663 | ( Reifies s (Compare ni) ) => | ||
664 | (ni -> Word -> Bool) | ||
665 | -> ni -> BitIx -> Bucket s ni -> [ Bucket s ni ] | ||
666 | splitTip 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. | ||
677 | modifyBucket | ||
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) | ||
682 | modifyBucket 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 | |||
703 | bktCount :: BucketList ni -> Int | ||
704 | bktCount (BucketList _ bkts) = L.length bkts | ||
705 | |||
706 | -- | Triggering event for atomic table update | ||
707 | data Event ni = TryInsert { foreignNode :: ni } | ||
708 | | PingResult { foreignNode :: ni , ponged :: Bool } | ||
709 | |||
710 | #if 0 | ||
711 | deriving instance Eq (NodeId) => Eq (Event) | ||
712 | deriving instance ( Show ip | ||
713 | , Show (NodeId) | ||
714 | , Show u | ||
715 | ) => Show (Event) | ||
716 | |||
717 | #endif | ||
718 | |||
719 | eventId :: (ni -> nid) -> Event ni -> nid | ||
720 | eventId nodeId (TryInsert ni) = nodeId ni | ||
721 | eventId nodeId (PingResult ni _) = nodeId ni | ||
722 | |||
723 | |||
724 | -- | Actions requested by atomic table update | ||
725 | data CheckPing ni = CheckPing [ni] | ||
726 | |||
727 | #if 0 | ||
728 | |||
729 | deriving instance Eq (NodeId) => Eq (CheckPing) | ||
730 | deriving 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 | -- | ||
750 | updateForInbound :: | ||
751 | KademliaSpace nid ni | ||
752 | -> Timestamp -> ni -> BucketList ni -> (Bool, [ni], BucketList ni) | ||
753 | updateForInbound 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. | ||
764 | updateForPingResult :: | ||
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 ) | ||
770 | updateForPingResult 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 | |||
782 | type TableEntry ni = (ni, Timestamp) | ||
783 | |||
784 | tableEntry :: NodeEntry ni -> TableEntry ni | ||
785 | tableEntry (a :-> b) = (a, b) | ||
786 | |||
787 | toList :: BucketList ni -> [[TableEntry ni]] | ||
788 | toList (BucketList _ bkts) = coerce $ L.map (L.map tableEntry . PSQ.toList . bktNodes) bkts | ||
789 | |||
790 | data 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 | |||
804 | instance 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 #-} | ||
7 | module Network.Kademlia.Search where | ||
8 | |||
9 | import Control.Concurrent.Tasks | ||
10 | import Control.Concurrent.STM | ||
11 | import Control.Exception | ||
12 | import Control.Monad | ||
13 | import Data.Bool | ||
14 | import Data.Function | ||
15 | import Data.List | ||
16 | import qualified Data.Map.Strict as Map | ||
17 | ;import Data.Map.Strict (Map) | ||
18 | import Data.Maybe | ||
19 | import qualified Data.Set as Set | ||
20 | ;import Data.Set (Set) | ||
21 | import Data.Hashable (Hashable(..)) -- for type sigs | ||
22 | import System.IO | ||
23 | import System.IO.Error | ||
24 | |||
25 | import qualified Data.MinMaxPSQ as MM | ||
26 | ;import Data.MinMaxPSQ (MinMaxPSQ, MinMaxPSQ') | ||
27 | import qualified Data.Wrapper.PSQ as PSQ | ||
28 | ;import Data.Wrapper.PSQ (pattern (:->), Binding, pattern Binding, Binding', PSQ, PSQKey) | ||
29 | import Network.Address hiding (NodeId) | ||
30 | import Network.Kademlia.Routing as R | ||
31 | #ifdef THREAD_DEBUG | ||
32 | import Control.Concurrent.Lifted.Instrument | ||
33 | #else | ||
34 | import Control.Concurrent.Lifted | ||
35 | import GHC.Conc (labelThread) | ||
36 | #endif | ||
37 | |||
38 | data 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 | |||
44 | data 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 | |||
72 | newSearch :: ( 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) | ||
87 | newSearch (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". | ||
99 | stripValue :: Binding' k p v -> Binding k p | ||
100 | stripValue (Binding ni _ nid) = (ni :-> nid) | ||
101 | |||
102 | -- | Reset a 'SearchState' object to ready it for a repeated search. | ||
103 | reset :: (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) | ||
109 | reset 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 | |||
121 | searchAlpha :: Int | ||
122 | searchAlpha = 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. | ||
133 | searchK :: Int | ||
134 | searchK = 16 | ||
135 | |||
136 | sendQuery :: 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 () | ||
148 | sendQuery 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 | |||
182 | searchIsFinished :: ( PSQKey nid | ||
183 | , PSQKey ni | ||
184 | ) => SearchState nid addr tok ni r -> STM Bool | ||
185 | searchIsFinished 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 | |||
195 | searchCancel :: SearchState nid addr tok ni r -> STM () | ||
196 | searchCancel SearchState{..} = do | ||
197 | writeTVar searchPendingCount 0 | ||
198 | writeTVar searchQueued MM.empty | ||
199 | |||
200 | search :: | ||
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) | ||
207 | search 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 | |||
213 | searchLoop :: ( 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 () | ||
219 | searchLoop 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 #-} | ||
11 | module Network.QueryResponse where | ||
12 | |||
13 | #ifdef THREAD_DEBUG | ||
14 | import Control.Concurrent.Lifted.Instrument | ||
15 | #else | ||
16 | import Control.Concurrent | ||
17 | import GHC.Conc (labelThread) | ||
18 | #endif | ||
19 | import Control.Concurrent.STM | ||
20 | import Control.Exception | ||
21 | import Control.Monad | ||
22 | import qualified Data.ByteString as B | ||
23 | ;import Data.ByteString (ByteString) | ||
24 | import Data.Function | ||
25 | import Data.Functor.Contravariant | ||
26 | import qualified Data.IntMap.Strict as IntMap | ||
27 | ;import Data.IntMap.Strict (IntMap) | ||
28 | import qualified Data.Map.Strict as Map | ||
29 | ;import Data.Map.Strict (Map) | ||
30 | import qualified Data.Word64Map as W64Map | ||
31 | ;import Data.Word64Map (Word64Map) | ||
32 | import Data.Word | ||
33 | import Data.Maybe | ||
34 | import Data.Typeable | ||
35 | import Network.Socket | ||
36 | import Network.Socket.ByteString as B | ||
37 | import System.Endian | ||
38 | import System.IO | ||
39 | import System.IO.Error | ||
40 | import System.Timeout | ||
41 | |||
42 | -- | Three methods are required to implement a datagram based query\/response protocol. | ||
43 | data 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. | ||
59 | layerTransportM :: | ||
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' | ||
69 | layerTransportM 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. | ||
82 | layerTransport :: | ||
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' | ||
92 | layerTransport 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. | ||
100 | partitionTransport :: ((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) | ||
104 | partitionTransport 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. | ||
110 | partitionTransportM :: ((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) | ||
114 | partitionTransportM 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 | ||
138 | addHandler :: (err -> IO ()) -> (addr -> x -> IO (Maybe (x -> x))) -> Transport err addr x -> Transport err addr x | ||
139 | addHandler 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. | ||
148 | onInbound :: (addr -> x -> IO ()) -> Transport err addr x -> Transport err addr x | ||
149 | onInbound 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 | ||
164 | forkListener :: String -> Transport err addr x -> IO (IO ()) | ||
165 | forkListener 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. | ||
176 | sendQuery :: | ||
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. | ||
183 | sendQuery (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. | ||
205 | data 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. | ||
230 | data 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_. | ||
241 | data 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 | |||
260 | contramapAddr :: (a -> b) -> MethodHandler err tid b x -> MethodHandler err tid a x | ||
261 | contramapAddr 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) | ||
266 | contramapAddr 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. | ||
273 | dispatchQuery :: 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)) | ||
279 | dispatchQuery (MethodHandler unwrapQ wrapR f) tid self x addr = | ||
280 | fmap (\a -> Just . wrapR tid self addr <$> f addr a) $ unwrapQ x | ||
281 | dispatchQuery (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'. | ||
287 | data 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. | ||
311 | data 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'. | ||
330 | data 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'. | ||
340 | intMapMethods :: TableMethods IntMap Int | ||
341 | intMapMethods = TableMethods IntMap.insert IntMap.delete IntMap.lookup | ||
342 | |||
343 | -- | Methods for using 'Data.Word64Map'. | ||
344 | w64MapMethods :: TableMethods Word64Map Word64 | ||
345 | w64MapMethods = TableMethods W64Map.insert W64Map.delete W64Map.lookup | ||
346 | |||
347 | -- | Methods for using 'Data.Map' | ||
348 | mapMethods :: Ord tid => TableMethods (Map tid) tid | ||
349 | mapMethods = 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. | ||
357 | instance 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. | ||
366 | transactionMethods :: | ||
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 | ||
370 | transactionMethods (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. | ||
384 | data 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'. | ||
405 | data 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 | |||
416 | ignoreErrors :: ErrorReporter addr x meth tid err | ||
417 | ignoreErrors = ErrorReporter | ||
418 | { reportParseError = \_ -> return () | ||
419 | , reportMissingHandler = \_ _ _ -> return () | ||
420 | , reportUnknown = \_ _ _ -> return () | ||
421 | , reportTimeout = \_ _ _ -> return () | ||
422 | } | ||
423 | |||
424 | printErrors :: ( Show addr | ||
425 | , Show meth | ||
426 | ) => Handle -> ErrorReporter addr x meth tid String | ||
427 | printErrors 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'. | ||
435 | instance 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. | ||
447 | handleMessage :: | ||
448 | Client err meth tid addr x | ||
449 | -> addr | ||
450 | -> x | ||
451 | -> IO (Maybe (x -> x)) | ||
452 | handleMessage (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. | ||
488 | sockAddrFamily :: SockAddr -> Family | ||
489 | sockAddrFamily (SockAddrInet _ _ ) = AF_INET | ||
490 | sockAddrFamily (SockAddrInet6 _ _ _ _) = AF_INET6 | ||
491 | sockAddrFamily (SockAddrUnix _ ) = AF_UNIX | ||
492 | sockAddrFamily (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. | ||
497 | ignoreEOF :: a -> IOError -> IO a | ||
498 | ignoreEOF 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'. | ||
503 | udpBufferSize :: Int | ||
504 | udpBufferSize = 65536 | ||
505 | |||
506 | -- | Wrapper around 'B.sendTo' that silently ignores DoesNotExistError. | ||
507 | saferSendTo :: Socket -> ByteString -> SockAddr -> IO () | ||
508 | saferSendTo 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'. | ||
521 | udpTransport :: SockAddr -> IO (Transport err SockAddr ByteString) | ||
522 | udpTransport 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. | ||
14 | module 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 | |||
28 | import Network.Socket | ||
29 | ( PortNumber | ||
30 | , SockAddr | ||
31 | ) | ||
32 | import Foreign.C.Types ( CUInt ) | ||
33 | |||
34 | import qualified Network.Socket as NS | ||
35 | import 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. | ||
40 | class 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 | |||
67 | instance 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. | ||
92 | data RestrictedSocket = Restricted (Maybe Handle) NS.Socket deriving Show | ||
93 | |||
94 | instance 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. | ||
117 | restrictSocket :: NS.Socket -> RestrictedSocket | ||
118 | restrictSocket 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'. | ||
123 | restrictHandleSocket :: Handle -> NS.Socket -> RestrictedSocket | ||
124 | restrictHandleSocket 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 #-} | ||
7 | module Network.StreamServer | ||
8 | ( streamServer | ||
9 | , ServerHandle | ||
10 | , ServerConfig(..) | ||
11 | , withSession | ||
12 | , quitListening | ||
13 | , dummyServerHandle | ||
14 | ) where | ||
15 | |||
16 | import Data.Monoid | ||
17 | import Network.Socket as Socket | ||
18 | import Data.ByteString.Char8 | ||
19 | ( hGetNonBlocking | ||
20 | ) | ||
21 | import qualified Data.ByteString.Char8 as S | ||
22 | ( hPutStrLn | ||
23 | ) | ||
24 | import System.Directory (removeFile) | ||
25 | import System.IO | ||
26 | ( IOMode(..) | ||
27 | , hSetBuffering | ||
28 | , BufferMode(..) | ||
29 | , hWaitForInput | ||
30 | , hClose | ||
31 | , hIsEOF | ||
32 | , hPutStrLn | ||
33 | , stderr | ||
34 | , hFlush | ||
35 | ) | ||
36 | import Control.Monad | ||
37 | import Control.Monad.Fix (fix) | ||
38 | #ifdef THREAD_DEBUG | ||
39 | import Control.Concurrent.Lifted.Instrument (forkIO, threadDelay, ThreadId, mkWeakThreadId, labelThread, myThreadId) | ||
40 | #else | ||
41 | import GHC.Conc (labelThread) | ||
42 | import Control.Concurrent (forkIO, threadDelay, ThreadId, mkWeakThreadId, myThreadId) | ||
43 | #endif | ||
44 | import Control.Exception (catch,handle,try,finally) | ||
45 | import System.IO.Error (tryIOError) | ||
46 | import System.Mem.Weak | ||
47 | import System.IO.Error | ||
48 | |||
49 | -- import Data.Conduit | ||
50 | import Control.Monad.IO.Class (MonadIO (liftIO)) | ||
51 | import qualified Data.ByteString as S (ByteString) | ||
52 | import System.IO (Handle) | ||
53 | import Control.Concurrent.MVar (newMVar) | ||
54 | |||
55 | import Network.SocketLike | ||
56 | |||
57 | data ServerHandle = ServerHandle Socket (Weak ThreadId) | ||
58 | |||
59 | |||
60 | -- | Create a useless do-nothing 'ServerHandle'. | ||
61 | dummyServerHandle :: IO ServerHandle | ||
62 | dummyServerHandle = 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 | |||
68 | removeSocketFile :: SockAddr -> IO () | ||
69 | removeSocketFile (SockAddrUnix fname) = removeFile fname | ||
70 | removeSocketFile _ = return () | ||
71 | |||
72 | -- | Terminate the server accept-loop. Call this to shut down the server. | ||
73 | quitListening :: ServerHandle -> IO () | ||
74 | quitListening (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.) | ||
81 | bshow :: Show a => a -> String | ||
82 | bshow e = show e | ||
83 | |||
84 | -- | Send a string to stderr. Not exported. Default 'serverWarn' when | ||
85 | -- 'withSession' is used to configure the server. | ||
86 | warnStderr :: String -> IO () | ||
87 | warnStderr str = hPutStrLn stderr str >> hFlush stderr | ||
88 | |||
89 | data 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. | ||
97 | withSession :: (RestrictedSocket -> Int -> Handle -> IO ()) -> ServerConfig | ||
98 | withSession 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. | ||
107 | streamServer :: ServerConfig -> SockAddr -> IO ServerHandle | ||
108 | streamServer 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'. | ||
130 | acceptLoop :: ServerConfig -> Socket -> Int -> IO () | ||
131 | acceptLoop 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 | |||
140 | acceptException :: ServerConfig -> Int -> Socket -> IOError -> IO () | ||
141 | acceptException 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 #-} | ||
15 | module Network.Tox where | ||
16 | |||
17 | import Debug.Trace | ||
18 | import Control.Exception hiding (Handler) | ||
19 | import Control.Applicative | ||
20 | import Control.Arrow | ||
21 | import Control.Concurrent (MVar) | ||
22 | import Control.Concurrent.STM | ||
23 | import Control.Monad | ||
24 | import Control.Monad.Fix | ||
25 | import qualified Crypto.Cipher.ChaChaPoly1305 as Symmetric | ||
26 | import qualified Crypto.Cipher.Salsa as Salsa | ||
27 | import qualified Crypto.Cipher.XSalsa as XSalsa | ||
28 | import Crypto.ECC.Class | ||
29 | import qualified Crypto.Error as Cryptonite | ||
30 | import Crypto.Error.Types | ||
31 | import qualified Crypto.MAC.Poly1305 as Poly1305 | ||
32 | import Crypto.PubKey.Curve25519 | ||
33 | import Crypto.PubKey.ECC.Types | ||
34 | import Crypto.Random | ||
35 | import qualified Data.Aeson as JSON | ||
36 | ;import Data.Aeson (FromJSON, ToJSON, (.=)) | ||
37 | import Data.Bitraversable (bisequence) | ||
38 | import Data.Bits | ||
39 | import Data.Bits.ByteString () | ||
40 | import Data.Bool | ||
41 | import qualified Data.ByteArray as BA | ||
42 | ;import Data.ByteArray (ByteArrayAccess, Bytes) | ||
43 | import qualified Data.ByteString as B | ||
44 | ;import Data.ByteString (ByteString) | ||
45 | import qualified Data.ByteString.Base16 as Base16 | ||
46 | import qualified Data.ByteString.Char8 as C8 | ||
47 | import Data.ByteString.Lazy (toStrict) | ||
48 | import Data.Char | ||
49 | import Data.Data | ||
50 | import Data.Functor.Contravariant | ||
51 | import Data.Hashable | ||
52 | import Data.IP | ||
53 | import Data.Maybe | ||
54 | import qualified Data.MinMaxPSQ as MinMaxPSQ | ||
55 | ;import Data.MinMaxPSQ (MinMaxPSQ') | ||
56 | import Data.Monoid | ||
57 | import Data.Ord | ||
58 | import qualified Data.Serialize as S | ||
59 | import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) | ||
60 | import Data.Typeable | ||
61 | import Data.Word | ||
62 | import qualified Data.Wrapper.PSQ as PSQ | ||
63 | ;import Data.Wrapper.PSQ (PSQ) | ||
64 | import qualified Data.Wrapper.PSQInt as Int | ||
65 | import Foreign.Marshal.Alloc | ||
66 | import Foreign.Ptr | ||
67 | import Foreign.Storable | ||
68 | import GHC.Generics (Generic) | ||
69 | import System.Global6 | ||
70 | import Network.Kademlia | ||
71 | import Network.Address (Address, WantIP (..), either4or6, | ||
72 | fromSockAddr, ipFamily, setPort, | ||
73 | sockAddrPort, testIdBit, | ||
74 | toSockAddr, un4map) | ||
75 | import Network.Kademlia.Search (Search (..)) | ||
76 | import qualified Network.Kademlia.Routing as R | ||
77 | import Network.QueryResponse | ||
78 | import Network.Socket | ||
79 | import System.Endian | ||
80 | import System.IO | ||
81 | import qualified Text.ParserCombinators.ReadP as RP | ||
82 | import Text.Printf | ||
83 | import Text.Read | ||
84 | import Control.TriadCommittee | ||
85 | import Network.BitTorrent.DHT.Token as Token | ||
86 | import GHC.TypeLits | ||
87 | |||
88 | import Crypto.Tox | ||
89 | import Data.Word64Map (fitsInInt) | ||
90 | import qualified Data.Word64Map (empty) | ||
91 | import Network.Kademlia.Bootstrap (forkPollForRefresh, bootstrap) | ||
92 | import Network.Tox.Crypto.Transport (NetCrypto) | ||
93 | import Network.Tox.Crypto.Handlers (cryptoNetHandler, newSessionsState, defaultUnRecHook, defaultCryptoDataHooks, NetCryptoSessions(..)) | ||
94 | import qualified Network.Tox.DHT.Handlers as DHT | ||
95 | import qualified Network.Tox.DHT.Transport as DHT | ||
96 | import Network.Tox.NodeId | ||
97 | import qualified Network.Tox.Onion.Handlers as Onion | ||
98 | import qualified Network.Tox.Onion.Transport as Onion | ||
99 | import Network.Tox.Transport | ||
100 | import OnionRouter | ||
101 | import Roster | ||
102 | import Text.XXD | ||
103 | |||
104 | newCrypto :: IO TransportCrypto | ||
105 | newCrypto = 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 | |||
140 | updateIP :: TVar (R.BucketList NodeInfo) -> SockAddr -> STM () | ||
141 | updateIP 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 | |||
147 | genNonce24 :: DRG g => | ||
148 | TVar (g, pending) -> DHT.TransactionId -> IO DHT.TransactionId | ||
149 | genNonce24 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 | |||
156 | gen :: forall gen. DRG gen => gen -> (DHT.TransactionId, gen) | ||
157 | gen 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 | |||
162 | intKey :: DHT.TransactionId -> Int | ||
163 | intKey (DHT.TransactionId (Nonce8 w) _) = fromIntegral w | ||
164 | |||
165 | w64Key :: DHT.TransactionId -> Word64 | ||
166 | w64Key (DHT.TransactionId (Nonce8 w) _) = w | ||
167 | |||
168 | nonceKey :: DHT.TransactionId -> Nonce8 | ||
169 | nonceKey (DHT.TransactionId n _) = n | ||
170 | |||
171 | -- | Return my own address. | ||
172 | myAddr :: TVar (R.BucketList NodeInfo) -- ^ IPv4 buckets | ||
173 | -> TVar (R.BucketList NodeInfo) -- ^ IPv6 buckets | ||
174 | -> Maybe NodeInfo -- ^ Interested remote address | ||
175 | -> IO NodeInfo | ||
176 | myAddr 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 | |||
183 | newClient :: (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) | ||
191 | newClient 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 | |||
224 | data 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 | |||
238 | getContactInfo :: Tox -> IO DHT.DHTPublicKey | ||
239 | getContactInfo 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 | |||
258 | isLocalHost :: SockAddr -> Bool | ||
259 | isLocalHost (SockAddrInet _ host32) = (fromBE32 host32 == 0x7f000001) | ||
260 | isLocalHost _ = False | ||
261 | |||
262 | addVerbosity :: Transport err SockAddr ByteString -> Transport err SockAddr ByteString | ||
263 | addVerbosity 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 | |||
277 | newKeysDatabase :: IO (TVar Onion.AnnouncedKeys) | ||
278 | newKeysDatabase = | ||
279 | atomically $ newTVar $ Onion.AnnouncedKeys PSQ.empty MinMaxPSQ.empty | ||
280 | |||
281 | |||
282 | getOnionAlias :: TransportCrypto -> STM NodeInfo -> Maybe (Onion.OnionDestination r) -> IO (Onion.OnionDestination r) | ||
283 | getOnionAlias 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 | |||
292 | newTox :: TVar Onion.AnnouncedKeys -> SockAddr -> Maybe NetCryptoSessions -> Maybe SecretKey -> IO Tox | ||
293 | newTox 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 | |||
354 | onionTimeout :: Tox -> DHT.TransactionId -> Onion.OnionDestination RouteId -> STM (Onion.OnionDestination RouteId, Int) | ||
355 | onionTimeout Tox { toxOnionRoutes = or } (DHT.TransactionId n8 _) od = lookupTimeout or n8 od | ||
356 | |||
357 | forkTox :: Tox -> IO (IO (), [NodeInfo] -> [NodeInfo] -> IO (), [NodeInfo] -> [NodeInfo] -> IO ()) | ||
358 | forkTox 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 #-} | ||
3 | module Network.Tox.Crypto.Handlers where | ||
4 | |||
5 | import Network.Tox.Crypto.Transport | ||
6 | import Network.Tox.DHT.Transport (Cookie(..),CookieData(..)) | ||
7 | import Crypto.Tox | ||
8 | import Control.Concurrent.STM | ||
9 | import Network.Address | ||
10 | import qualified Data.Map.Strict as Map | ||
11 | import Crypto.Hash | ||
12 | import Control.Applicative | ||
13 | import Control.Monad | ||
14 | import Data.Time.Clock.POSIX | ||
15 | import qualified Data.ByteString as B | ||
16 | import Control.Lens | ||
17 | import Data.Function | ||
18 | import Data.Serialize as S | ||
19 | import Data.Word | ||
20 | import GHC.Conc (unsafeIOToSTM) | ||
21 | import qualified Data.Set as Set | ||
22 | |||
23 | -- util, todo: move to another module | ||
24 | maybeToEither :: Maybe b -> Either String b | ||
25 | maybeToEither (Just x) = Right x | ||
26 | maybeToEither Nothing = Left "maybeToEither" | ||
27 | |||
28 | data NetCryptoSessionStatus = Unaccepted | Accepted | Confirmed | ||
29 | deriving (Eq,Ord,Show,Enum) | ||
30 | |||
31 | |||
32 | type IOHook addr x = addr -> x -> IO (Maybe (x -> x)) | ||
33 | type NetCryptoHook = IOHook NetCryptoSession CryptoData | ||
34 | |||
35 | |||
36 | data 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 | |||
53 | data NetCryptoSessions = NCSessions { netCryptoSessions :: TVar (Map.Map SockAddr NetCryptoSession) | ||
54 | , transportCrypto :: TransportCrypto | ||
55 | , defaultHooks :: Map.Map MessageType [NetCryptoHook] | ||
56 | , defaultUnrecognizedHook :: MessageType -> NetCryptoHook | ||
57 | } | ||
58 | |||
59 | newSessionsState :: TransportCrypto -> (MessageType -> NetCryptoHook) -> Map.Map MessageType [NetCryptoHook] -> IO NetCryptoSessions | ||
60 | newSessionsState 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 | |||
68 | data 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 | } | ||
77 | newHandShakeData :: TransportCrypto -> HandshakeParams -> HandshakeData | ||
78 | newHandShakeData = error "todo" | ||
79 | |||
80 | -- | called when we recieve a crypto handshake with valid cookie | ||
81 | freshCryptoSession :: NetCryptoSessions -> SockAddr -> HandshakeParams -> IO () | ||
82 | freshCryptoSession 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. | ||
130 | updateCryptoSession :: NetCryptoSessions -> SockAddr -> HandshakeParams -> NetCryptoSession -> IO () | ||
131 | updateCryptoSession 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 | |||
148 | cryptoNetHandler :: NetCryptoSessions -> SockAddr -> NetCrypto -> IO (Maybe (NetCrypto -> NetCrypto)) | ||
149 | cryptoNetHandler 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 | |||
203 | cryptoNetHandler 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 | ||
273 | defaultCryptoDataHooks :: Map.Map MessageType [NetCryptoHook] | ||
274 | defaultCryptoDataHooks = Map.empty | ||
275 | |||
276 | -- | discards all unrecognized packets | ||
277 | defaultUnRecHook :: MessageType -> NetCryptoHook | ||
278 | defaultUnRecHook _ _ _ = return Nothing | ||
279 | |||
280 | -- | use to add a single hook to a specific session. | ||
281 | addCryptoDataHook1 :: Map.Map MessageType [NetCryptoHook] -> MessageType -> NetCryptoHook -> Map.Map MessageType [NetCryptoHook] | ||
282 | addCryptoDataHook1 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 #-} | ||
7 | module 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 | |||
51 | import Crypto.Tox | ||
52 | import Network.Tox.DHT.Transport (Cookie) | ||
53 | import Network.Tox.NodeId | ||
54 | |||
55 | import Network.Socket | ||
56 | import Data.ByteArray | ||
57 | |||
58 | import Data.ByteString as B | ||
59 | import Data.Maybe | ||
60 | import Data.Monoid | ||
61 | import Data.Word | ||
62 | import Crypto.Hash | ||
63 | import Control.Lens | ||
64 | import Data.Text as T | ||
65 | import Data.Text.Encoding as T | ||
66 | import Data.Serialize as S | ||
67 | import Control.Arrow | ||
68 | |||
69 | |||
70 | data NetCrypto | ||
71 | = NetHandshake (Handshake Encrypted) | ||
72 | | NetCrypto (CryptoPacket Encrypted) | ||
73 | |||
74 | parseNetCrypto :: ByteString -> SockAddr -> Either String (NetCrypto, SockAddr) | ||
75 | parseNetCrypto pkt@(B.uncons -> Just (0x1a,_)) saddr = left ("parseNetCrypto: "++) $ (,saddr) . NetHandshake <$> runGet get pkt | ||
76 | parseNetCrypto pkt@(B.uncons -> Just (0x1b,_)) saddr = left ("parseNetCrypto: "++) $ (,saddr) . NetCrypto <$> runGet get pkt | ||
77 | parseNetCrypto _ _ = Left "parseNetCrypto: ?" | ||
78 | |||
79 | encodeNetCrypto :: NetCrypto -> SockAddr -> (ByteString, SockAddr) | ||
80 | encodeNetCrypto (NetHandshake x) saddr = (B.cons 0x1a (runPut $ put x),saddr) | ||
81 | encodeNetCrypto (NetCrypto x) saddr = (B.cons 0x1b (runPut $ put x),saddr) | ||
82 | |||
83 | data 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 | |||
98 | instance Serialize (Handshake Encrypted) where | ||
99 | get = Handshake <$> get <*> get <*> get | ||
100 | put (Handshake cookie n24 dta) = put cookie >> put n24 >> put dta | ||
101 | |||
102 | data HandshakeData = HandshakeData | ||
103 | { baseNonce :: Nonce24 | ||
104 | , sessionKey :: PublicKey | ||
105 | , cookieHash :: Digest SHA512 | ||
106 | , otherCookie :: Cookie | ||
107 | } | ||
108 | |||
109 | instance 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 | |||
115 | instance 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 | |||
126 | data 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 | |||
135 | instance Sized CryptoData where | ||
136 | size = contramap bufferStart size | ||
137 | <> contramap bufferEnd size | ||
138 | <> contramap bufferData size | ||
139 | |||
140 | instance Serialize (CryptoPacket Encrypted) where | ||
141 | get = CryptoPacket <$> get <*> get | ||
142 | put (CryptoPacket n16 dta) = put n16 >> put dta | ||
143 | |||
144 | data 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 | |||
154 | instance 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. | ||
170 | data UserStatus = Online | Away | Busy deriving (Show,Eq,Ord,Enum) | ||
171 | |||
172 | data TypingStatus = NotTyping | Typing deriving (Show,Eq,Ord,Enum) | ||
173 | |||
174 | data CryptoMessage | ||
175 | = OneByte { msgID :: MessageID } | ||
176 | | TwoByte { msgID :: MessageID, msgByte :: Word8 } | ||
177 | | UpToN { msgID :: MessageID, msgBytes :: ByteString } -- length < N | ||
178 | deriving (Eq,Show) | ||
179 | |||
180 | instance Sized CryptoMessage where | ||
181 | size = VarSize $ \case | ||
182 | OneByte {} -> 1 | ||
183 | TwoByte {} -> 2 | ||
184 | UpToN { msgBytes = bs } -> 1 + B.length bs | ||
185 | |||
186 | instance 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 | |||
201 | instance Serialize MessageID where | ||
202 | get = toEnum . fromIntegral <$> getWord8 | ||
203 | put x = putWord8 (fromIntegral . fromEnum $ x) | ||
204 | |||
205 | erCompat :: String -> a | ||
206 | erCompat lens = error $ "Use of '" ++ lens ++ "' lens on incompatible CryptoMessage type" | ||
207 | |||
208 | typingStatus :: Functor f => (UserStatus -> f UserStatus)-> (CryptoMessage -> f CryptoMessage) | ||
209 | typingStatus = 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 | |||
218 | userStatus :: Functor f => (UserStatus -> f UserStatus)-> (CryptoMessage -> f CryptoMessage) | ||
219 | userStatus = 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 | |||
226 | nick :: Functor f => (Text -> f Text)-> (CryptoMessage -> f CryptoMessage) | ||
227 | nick = 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 | |||
234 | statusMessage :: Functor f => (String -> f String)-> (CryptoMessage -> f CryptoMessage) | ||
235 | statusMessage = 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 | |||
242 | action :: Functor f => (String -> f String)-> (CryptoMessage -> f CryptoMessage) | ||
243 | action = 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 | |||
250 | newtype GroupChatId = GrpId ByteString -- 33 bytes | ||
251 | deriving (Show,Eq) | ||
252 | |||
253 | class HasGroupChatID x where | ||
254 | getGroupChatID :: x -> GroupChatId | ||
255 | setGroupChatID :: x -> GroupChatId -> x | ||
256 | |||
257 | sizedN :: Int -> ByteString -> ByteString | ||
258 | sizedN 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 | |||
261 | sizedAtLeastN :: Int -> ByteString -> ByteString | ||
262 | sizedAtLeastN n bs = if B.length bs < n then B.append bs (B.replicate (n - B.length bs) 0) | ||
263 | else bs | ||
264 | |||
265 | instance 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 | |||
290 | groupChatID :: (Functor f, HasGroupChatID x) => (GroupChatId -> f GroupChatId) -> (x -> f x) | ||
291 | groupChatID = lens getGroupChatID setGroupChatID | ||
292 | |||
293 | type GroupNumber = Word16 | ||
294 | type PeerNumber = Word16 | ||
295 | type MessageNumber = Word32 | ||
296 | |||
297 | class HasGroupNumber x where | ||
298 | getGroupNumber :: x -> GroupNumber | ||
299 | setGroupNumber :: x -> GroupNumber -> x | ||
300 | |||
301 | instance 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 | |||
320 | groupNumber :: (Functor f, HasGroupNumber x) => (Word16 -> f Word16) -> (x -> f x) | ||
321 | groupNumber = lens getGroupNumber setGroupNumber | ||
322 | |||
323 | class HasGroupNumberToJoin x where | ||
324 | getGroupNumberToJoin :: x -> GroupNumber | ||
325 | setGroupNumberToJoin :: x -> GroupNumber -> x | ||
326 | |||
327 | instance 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 | |||
340 | groupNumberToJoin :: (Functor f, HasGroupNumberToJoin x) => (GroupNumber -> f GroupNumber) -> (x -> f x) | ||
341 | groupNumberToJoin = lens getGroupNumberToJoin setGroupNumberToJoin | ||
342 | |||
343 | class HasPeerNumber x where | ||
344 | getPeerNumber :: x -> PeerNumber | ||
345 | setPeerNumber :: x -> PeerNumber -> x | ||
346 | |||
347 | instance 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 | |||
360 | peerNumber :: (Functor f, HasPeerNumber x) => (Word16 -> f Word16) -> (x -> f x) | ||
361 | peerNumber = lens getPeerNumber setPeerNumber | ||
362 | |||
363 | class HasMessageNumber x where | ||
364 | getMessageNumber :: x -> MessageNumber | ||
365 | setMessageNumber :: x -> MessageNumber -> x | ||
366 | |||
367 | instance 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 | |||
380 | messageNumber :: (Functor f, HasMessageNumber x) => (Word32 -> f Word32) -> (x -> f x) | ||
381 | messageNumber = lens getMessageNumber setMessageNumber | ||
382 | |||
383 | |||
384 | class HasMessageName x where | ||
385 | getMessageName :: x -> MessageName | ||
386 | setMessageName :: x -> MessageName -> x | ||
387 | |||
388 | instance 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 | |||
403 | messageName :: (Functor f, HasMessageName x) => (MessageName -> f MessageName) -> (x -> f x) | ||
404 | messageName = lens getMessageName setMessageName | ||
405 | |||
406 | data MessageType = Msg MessageID | ||
407 | | GrpMsg MessageName | ||
408 | deriving (Eq,Show) | ||
409 | |||
410 | instance 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 | |||
416 | class HasMessageType x where | ||
417 | getMessageType :: x -> MessageType | ||
418 | setMessageType :: x -> MessageType -> x | ||
419 | |||
420 | instance 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 | |||
438 | instance 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 | ||
443 | messageType :: (Functor f, HasMessageType x) => (MessageType -> f MessageType) -> (x -> f x) | ||
444 | messageType = lens getMessageType setMessageType | ||
445 | |||
446 | type MessageData = B.ByteString | ||
447 | |||
448 | class HasMessageData x where | ||
449 | getMessageData :: x -> MessageData | ||
450 | setMessageData :: x -> MessageData -> x | ||
451 | |||
452 | instance 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 | |||
468 | messageData :: (Functor f, HasMessageData x) => (MessageData -> f MessageData) -> (x -> f x) | ||
469 | messageData = lens getMessageData setMessageData | ||
470 | |||
471 | class HasTitle x where | ||
472 | getTitle :: x -> Text | ||
473 | setTitle :: x -> Text -> x | ||
474 | |||
475 | instance 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 | |||
487 | title :: (Functor f, HasTitle x) => (Text -> f Text) -> (x -> f x) | ||
488 | title = lens getTitle setTitle | ||
489 | |||
490 | class HasMessage x where | ||
491 | getMessage :: x -> Text | ||
492 | setMessage :: x -> Text -> x | ||
493 | |||
494 | instance 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 | |||
505 | message :: (Functor f, HasMessage x) => (Text -> f Text) -> (x -> f x) | ||
506 | message = lens getMessage setMessage | ||
507 | |||
508 | class HasName x where | ||
509 | getName :: x -> Text | ||
510 | setName :: x -> Text -> x | ||
511 | |||
512 | |||
513 | instance 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 | |||
523 | name :: (Functor f, HasTitle x) => (Text -> f Text) -> (x -> f x) | ||
524 | name = lens getTitle setTitle | ||
525 | |||
526 | data PeerInfo | ||
527 | = PeerInfo | ||
528 | { piPeerNum :: PeerNumber | ||
529 | , piUserKey :: PublicKey | ||
530 | , piDHTKey :: PublicKey | ||
531 | , piName :: ByteString -- byte-prefix for length | ||
532 | } deriving (Eq,Show) | ||
533 | |||
534 | instance HasPeerNumber PeerInfo where | ||
535 | getPeerNumber = piPeerNum | ||
536 | setPeerNumber x n = x { piPeerNum = n } | ||
537 | |||
538 | instance 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 | -- | ||
563 | msg :: MessageID -> CryptoMessage | ||
564 | msg mid | Just (True,0) <- msgSizeParam mid = OneByte mid | ||
565 | msg mid | Just (True,1) <- msgSizeParam mid = TwoByte mid 0 | ||
566 | msg mid | Just (False,_) <- msgSizeParam mid = UpToN mid B.empty | ||
567 | msg mid = UpToN mid B.empty | ||
568 | |||
569 | leaveMsg :: Serialize a => a -> CryptoMessage | ||
570 | leaveMsg groupnum = UpToN DIRECT_GROUPCHAT (B.snoc (S.encode groupnum) 0x01) | ||
571 | |||
572 | peerQueryMsg :: Serialize a => a -> CryptoMessage | ||
573 | peerQueryMsg 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. | ||
579 | msgSizeParam :: MessageID -> Maybe (Bool,Int) | ||
580 | msgSizeParam ONLINE = Just (True,0) | ||
581 | msgSizeParam OFFLINE = Just (True,0) | ||
582 | msgSizeParam USERSTATUS = Just (True,1) | ||
583 | msgSizeParam TYPING = Just (True,1) | ||
584 | msgSizeParam NICKNAME = Just (False,128) | ||
585 | msgSizeParam STATUSMESSAGE = Just (False,1007) | ||
586 | msgSizeParam MESSAGE = Just (False,1372) | ||
587 | msgSizeParam ACTION = Just (False,1372) | ||
588 | msgSizeParam FILE_DATA = Just (False,1372)-- up to 1373 | ||
589 | msgSizeParam FILE_SENDREQUEST = Just (False,300) -- 1+1+4+8+32+max255 = up to 301 | ||
590 | msgSizeParam FILE_CONTROL = Just (False,7) -- 8 bytes if seek, otherwise 4 | ||
591 | msgSizeParam INVITE_GROUPCHAT = Just (False,38) | ||
592 | msgSizeParam ONLINE_PACKET = Just (True,35) | ||
593 | msgSizeParam DIRECT_GROUPCHAT {-0x62-} = Nothing -- 1+2+1 thus Just (True,3) leave & peer-query, but variable in response packets | ||
594 | msgSizeParam MESSAGE_GROUPCHAT {-0x63-} = Nothing -- variable | ||
595 | msgSizeParam LOSSY_GROUPCHAT {-0xC7-} = Nothing -- variable | ||
596 | msgSizeParam _ = Nothing | ||
597 | |||
598 | isIndirectGrpChat :: MessageID -> Bool | ||
599 | isIndirectGrpChat MESSAGE_GROUPCHAT = True | ||
600 | isIndirectGrpChat LOSSY_GROUPCHAT = True | ||
601 | isIndirectGrpChat _ = False | ||
602 | |||
603 | data LossyOrLossless = UnknownLossyness | Lossless | Lossy | ||
604 | deriving (Eq,Ord,Enum,Show,Bounded) | ||
605 | |||
606 | lossyness :: MessageID -> LossyOrLossless | ||
607 | lossyness (fromEnum -> x) | x < 3 = Lossy | ||
608 | lossyness (fromEnum -> x) | x >= 16, x < 192 = Lossless | ||
609 | lossyness (fromEnum -> x) | x >= 192, x < 255 = Lossy | ||
610 | lossyness (fromEnum -> 255) = Lossless | ||
611 | lossyness _ = UnknownLossyness | ||
612 | |||
613 | -- TODO: Flesh this out. | ||
614 | data 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 | |||
875 | data 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 #-} | ||
6 | module Network.Tox.DHT.Handlers where | ||
7 | |||
8 | import Network.Tox.DHT.Transport as DHTTransport | ||
9 | import Network.QueryResponse as QR hiding (Client) | ||
10 | import qualified Network.QueryResponse as QR (Client) | ||
11 | import Crypto.Tox | ||
12 | import Network.Kademlia.Search | ||
13 | import qualified Data.Wrapper.PSQInt as Int | ||
14 | import Network.Kademlia | ||
15 | import Network.Kademlia.Bootstrap | ||
16 | import Network.Address (WantIP (..), ipFamily, testIdBit,fromSockAddr, sockAddrPort) | ||
17 | import qualified Network.Kademlia.Routing as R | ||
18 | import Control.TriadCommittee | ||
19 | import System.Global6 | ||
20 | import OnionRouter | ||
21 | |||
22 | import qualified Data.ByteArray as BA | ||
23 | import qualified Data.ByteString.Char8 as C8 | ||
24 | import qualified Data.ByteString.Base16 as Base16 | ||
25 | import Control.Arrow | ||
26 | import Control.Monad | ||
27 | import Control.Concurrent.Lifted.Instrument | ||
28 | import Control.Concurrent.STM | ||
29 | import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) | ||
30 | import Network.Socket | ||
31 | import Data.Hashable | ||
32 | import Data.IP | ||
33 | import Data.Ord | ||
34 | import Data.Maybe | ||
35 | import Data.Bits | ||
36 | import Data.Serialize (Serialize) | ||
37 | import Data.Word | ||
38 | import Data.List | ||
39 | import System.IO | ||
40 | |||
41 | data 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 | |||
47 | newtype PacketKind = PacketKind Word8 | ||
48 | deriving (Eq, Ord, Serialize) | ||
49 | |||
50 | pattern OnionRequest0Type = PacketKind 128 -- 0x80 Onion Request 0 | ||
51 | pattern OnionRequest1Type = PacketKind 129 -- 0x81 Onion Request 1 | ||
52 | pattern OnionRequest2Type = PacketKind 130 -- 0x82 Onion Request 2 | ||
53 | pattern AnnounceType = PacketKind 131 -- 0x83 Announce Request | ||
54 | pattern AnnounceResponseType = PacketKind 132 -- 0x84 Announce Response | ||
55 | |||
56 | pattern DataRequestType = PacketKind 133 -- 0x85 Onion Data Request (data to route request packet) | ||
57 | pattern DataResponseType = PacketKind 134 -- 0x86 Onion Data Response (data to route response packet) | ||
58 | -- 0x8c Onion Response 3 | ||
59 | -- 0x8d Onion Response 2 | ||
60 | pattern OnionResponse3Type = PacketKind 140 -- 0x8c Onion Response 3 | ||
61 | pattern OnionResponse2Type = PacketKind 141 -- 0x8d Onion Response 2 | ||
62 | pattern OnionResponse1Type = PacketKind 142 -- 0x8e Onion Response 1 | ||
63 | -- 0xf0 Bootstrap Info | ||
64 | |||
65 | pattern DHTRequestType = PacketKind 32 -- 0x20 DHT Request | ||
66 | |||
67 | pattern CookieRequestType = PacketKind 0x18 | ||
68 | pattern CookieResponseType = PacketKind 0x19 | ||
69 | |||
70 | pattern PingType = PacketKind 0 -- 0x00 Ping Request | ||
71 | pattern PongType = PacketKind 1 -- 0x01 Ping Response | ||
72 | pattern GetNodesType = PacketKind 2 -- 0x02 Nodes Request | ||
73 | pattern SendNodesType = PacketKind 4 -- 0x04 Nodes Response | ||
74 | |||
75 | |||
76 | instance 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 | |||
93 | msgType :: ( 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 | ||
98 | msgType msg = PacketKind $ fst $ dhtMessageType msg | ||
99 | |||
100 | classify :: Client -> Message -> MessageClass String PacketKind TransactionId NodeInfo Message | ||
101 | classify client (DHTLanDiscovery {}) = IsUnsolicited (lanDiscoveryH client) | ||
102 | classify 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 | |||
113 | data 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 | |||
121 | sched4 :: Routing -> TVar (Int.PSQ POSIXTime) | ||
122 | sched4 Routing { refresher4 = BucketRefresher { refreshQueue } } = refreshQueue | ||
123 | |||
124 | sched6 :: Routing -> TVar (Int.PSQ POSIXTime) | ||
125 | sched6 Routing { refresher6 = BucketRefresher { refreshQueue } } = refreshQueue | ||
126 | |||
127 | routing4 :: Routing -> TVar (R.BucketList NodeInfo) | ||
128 | routing4 Routing { refresher4 = BucketRefresher { refreshBuckets } } = refreshBuckets | ||
129 | |||
130 | routing6 :: Routing -> TVar (R.BucketList NodeInfo) | ||
131 | routing6 Routing { refresher6 = BucketRefresher { refreshBuckets } } = refreshBuckets | ||
132 | |||
133 | newRouting :: 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) | ||
137 | newRouting 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 | ||
174 | isLocal :: IP -> Bool | ||
175 | isLocal (IPv6 ip6) = (ip6 == toEnum 0) | ||
176 | isLocal (IPv4 ip4) = (ip4 == toEnum 0) | ||
177 | |||
178 | isGlobal :: IP -> Bool | ||
179 | isGlobal = not . isLocal | ||
180 | |||
181 | prefer4or6 :: NodeInfo -> Maybe WantIP -> WantIP | ||
182 | prefer4or6 addr iptyp = fromMaybe (ipFamily $ nodeIP addr) iptyp | ||
183 | |||
184 | toxSpace :: R.KademliaSpace NodeId NodeInfo | ||
185 | toxSpace = R.KademliaSpace | ||
186 | { R.kademliaLocation = nodeId | ||
187 | , R.kademliaTestBit = testNodeIdBit | ||
188 | , R.kademliaXor = xorNodeId | ||
189 | , R.kademliaSample = sampleNodeId | ||
190 | } | ||
191 | |||
192 | |||
193 | pingH :: NodeInfo -> Ping -> IO Pong | ||
194 | pingH _ Ping = return Pong | ||
195 | |||
196 | getNodesH :: Routing -> NodeInfo -> GetNodes -> IO SendNodes | ||
197 | getNodesH 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 | |||
220 | cookieRequestH :: TransportCrypto -> NodeInfo -> CookieRequest -> IO Cookie | ||
221 | cookieRequestH 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 | |||
238 | lanDiscoveryH :: Client -> NodeInfo -> NodeInfo -> IO (Maybe (Message -> Message)) | ||
239 | lanDiscoveryH client _ ni = do | ||
240 | forkIO $ do | ||
241 | myThreadId >>= flip labelThread "lan-discover-ping" | ||
242 | ping client ni | ||
243 | return () | ||
244 | return Nothing | ||
245 | |||
246 | type Message = DHTMessage ((,) Nonce8) | ||
247 | |||
248 | type Client = QR.Client String PacketKind TransactionId NodeInfo Message | ||
249 | |||
250 | |||
251 | wrapAsymm :: TransactionId -> NodeInfo -> NodeInfo -> (Nonce8 -> dta) -> Asymm dta | ||
252 | wrapAsymm (TransactionId n8 n24) src dst dta = Asymm | ||
253 | { senderKey = id2key $ nodeId src | ||
254 | , asymmNonce = n24 | ||
255 | , asymmData = dta n8 | ||
256 | } | ||
257 | |||
258 | serializer :: PacketKind | ||
259 | -> (Asymm (Nonce8,ping) -> Message) | ||
260 | -> (Message -> Maybe (Asymm (Nonce8,pong))) | ||
261 | -> MethodSerializer TransactionId NodeInfo Message PacketKind ping (Maybe pong) | ||
262 | serializer 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 | |||
272 | unpong :: Message -> Maybe (Asymm (Nonce8,Pong)) | ||
273 | unpong (DHTPong asymm) = Just asymm | ||
274 | unpong _ = Nothing | ||
275 | |||
276 | showHex :: BA.ByteArrayAccess ba => ba -> String | ||
277 | showHex bs = C8.unpack $ Base16.encode $ BA.convert bs | ||
278 | |||
279 | ping :: Client -> NodeInfo -> IO Bool | ||
280 | ping 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 | |||
287 | saveCookieKey :: TVar [(SockAddr, (Int, PublicKey))] -> SockAddr -> PublicKey -> STM () | ||
288 | saveCookieKey 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 | |||
297 | loseCookieKey :: TVar [(SockAddr, (Int, PublicKey))] -> SockAddr -> PublicKey -> STM () | ||
298 | loseCookieKey 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 | |||
306 | cookieRequest :: TransportCrypto -> Client -> PublicKey -> NodeInfo -> IO (Maybe Cookie) | ||
307 | cookieRequest 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 | |||
325 | unCookie :: DHTMessage t -> Maybe (t Cookie) | ||
326 | unCookie (DHTCookie n24 fcookie) = Just fcookie | ||
327 | unCookie _ = Nothing | ||
328 | |||
329 | unsendNodes :: Message -> Maybe (Asymm (Nonce8,SendNodes)) | ||
330 | unsendNodes (DHTSendNodes asymm) = Just asymm | ||
331 | unsendNodes _ = Nothing | ||
332 | |||
333 | unwrapNodes :: SendNodes -> ( [NodeInfo], [NodeInfo], Maybe () ) | ||
334 | unwrapNodes (SendNodes ns) = (ns,ns,Just ()) | ||
335 | |||
336 | getNodes :: Client -> NodeId -> NodeInfo -> IO (Maybe ([NodeInfo],[NodeInfo],Maybe ())) | ||
337 | getNodes 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 | |||
343 | updateRouting :: Client -> Routing -> OnionRouter -> NodeInfo -> Message -> IO () | ||
344 | updateRouting 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 | |||
352 | updateTable :: Client -> NodeInfo -> OnionRouter -> TriadCommittee NodeId SockAddr -> BucketRefresher NodeId NodeInfo -> IO () | ||
353 | updateTable 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 | |||
359 | toxKademlia :: Client -> TriadCommittee NodeId SockAddr -> OnionRouter | ||
360 | -> BucketRefresher NodeId NodeInfo | ||
361 | -> Kademlia NodeId NodeInfo | ||
362 | toxKademlia 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 | |||
381 | transitionCommittee :: TriadCommittee NodeId SockAddr -> RoutingTransition NodeInfo -> STM (IO ()) | ||
382 | transitionCommittee committee (RoutingTransition ni Stranger) = do | ||
383 | delVote committee (nodeId ni) | ||
384 | return $ do | ||
385 | -- hPutStrLn stderr $ "delVote "++show (nodeId ni) | ||
386 | return () | ||
387 | transitionCommittee committee _ = return $ return () | ||
388 | |||
389 | type Handler = MethodHandler String TransactionId NodeInfo Message | ||
390 | |||
391 | isPing :: (f Ping -> Ping) -> DHTMessage f -> Either String Ping | ||
392 | isPing unpack (DHTPing a) = Right $ unpack $ asymmData a | ||
393 | isPing _ _ = Left "Bad ping" | ||
394 | |||
395 | mkPong :: TransactionId -> NodeInfo -> NodeInfo -> Pong -> DHTMessage ((,) Nonce8) | ||
396 | mkPong tid src dst pong = DHTPong $ wrapAsymm tid src dst (, pong) | ||
397 | |||
398 | isGetNodes :: (f GetNodes -> GetNodes) -> DHTMessage f -> Either String GetNodes | ||
399 | isGetNodes unpack (DHTGetNodes a) = Right $ unpack $ asymmData a | ||
400 | isGetNodes _ _ = Left "Bad GetNodes" | ||
401 | |||
402 | mkSendNodes :: TransactionId -> NodeInfo -> NodeInfo -> SendNodes -> DHTMessage ((,) Nonce8) | ||
403 | mkSendNodes tid src dst sendnodes = DHTSendNodes $ wrapAsymm tid src dst (, sendnodes) | ||
404 | |||
405 | isCookieRequest :: (f CookieRequest -> CookieRequest) -> DHTMessage f -> Either String CookieRequest | ||
406 | isCookieRequest unpack (DHTCookieRequest a) = Right $ unpack $ asymmData a | ||
407 | isCookieRequest _ _ = Left "Bad cookie request" | ||
408 | |||
409 | mkCookie :: TransactionId -> NodeInfo -> NodeInfo -> Cookie -> DHTMessage ((,) Nonce8) | ||
410 | mkCookie (TransactionId n8 n24) src dst cookie = DHTCookie n24 (n8,cookie) | ||
411 | |||
412 | isDHTRequest :: (f DHTRequest -> DHTRequest) -> DHTMessage f -> Either String DHTRequest | ||
413 | isDHTRequest unpack (DHTDHTRequest pubkey a) = Right $ unpack $ asymmData a | ||
414 | isDHTRequest _ _ = Left "Bad dht relay request" | ||
415 | |||
416 | dhtRequestH :: NodeInfo -> DHTRequest -> IO () | ||
417 | dhtRequestH ni req = do | ||
418 | hPutStrLn stderr $ "Unhandled DHT Request: " ++ show req | ||
419 | |||
420 | handlers :: TransportCrypto -> Routing -> PacketKind -> Maybe Handler | ||
421 | handlers _ routing PingType = Just $ MethodHandler (isPing snd) mkPong $ pingH | ||
422 | handlers _ routing GetNodesType = Just $ MethodHandler (isGetNodes snd) mkSendNodes $ getNodesH routing | ||
423 | handlers crypto _ CookieRequestType = Just $ MethodHandler (isCookieRequest snd) mkCookie $ cookieRequestH crypto | ||
424 | handlers _ _ DHTRequestType = Just $ NoReply (isDHTRequest snd) $ dhtRequestH | ||
425 | handlers _ _ typ = error $ "TODO DHT handlers " ++ show typ | ||
426 | |||
427 | nodeSearch :: Client -> Search NodeId (IP,PortNumber) () NodeInfo NodeInfo | ||
428 | nodeSearch 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 #-} | ||
9 | module 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 | |||
35 | import Network.Tox.NodeId | ||
36 | import Crypto.Tox hiding (encrypt,decrypt) | ||
37 | import qualified Crypto.Tox as ToxCrypto | ||
38 | import Network.QueryResponse | ||
39 | |||
40 | import Control.Arrow | ||
41 | import Control.Concurrent.STM | ||
42 | import Control.Monad | ||
43 | import Data.Bool | ||
44 | import qualified Data.ByteString.Char8 as B8 | ||
45 | import qualified Data.ByteString as B | ||
46 | ;import Data.ByteString (ByteString) | ||
47 | import Data.Functor.Contravariant | ||
48 | import Data.Maybe | ||
49 | import Data.Monoid | ||
50 | import Data.Serialize as S | ||
51 | import Data.Tuple | ||
52 | import Data.Word | ||
53 | import Foreign.C (CTime(..)) | ||
54 | import Network.Socket | ||
55 | import qualified Data.ByteString.Base64 as Base64 | ||
56 | import qualified Data.ByteString.Base16 as Base16 | ||
57 | import Data.Char (isSpace) | ||
58 | |||
59 | type DHTTransport = Transport String NodeInfo (DHTMessage Encrypted8) | ||
60 | type HandleHi a = Maybe (Either String (DHTMessage Encrypted8, NodeInfo)) -> IO a | ||
61 | |||
62 | |||
63 | data 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 | |||
73 | deriving 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 | |||
82 | mapMessage :: forall f b. (forall a. Nonce24 -> f a -> b) -> DHTMessage f -> Maybe b | ||
83 | mapMessage f (DHTPing a) = Just $ f (asymmNonce a) (asymmData a) | ||
84 | mapMessage f (DHTPong a) = Just $ f (asymmNonce a) (asymmData a) | ||
85 | mapMessage f (DHTGetNodes a) = Just $ f (asymmNonce a) (asymmData a) | ||
86 | mapMessage f (DHTSendNodes a) = Just $ f (asymmNonce a) (asymmData a) | ||
87 | mapMessage f (DHTCookieRequest a) = Just $ f (asymmNonce a) (asymmData a) | ||
88 | mapMessage f (DHTDHTRequest _ a) = Just $ f (asymmNonce a) (asymmData a) | ||
89 | mapMessage f (DHTCookie nonce fcookie) = Just $ f nonce fcookie | ||
90 | mapMessage f (DHTLanDiscovery nid) = Nothing | ||
91 | |||
92 | |||
93 | instance Sized Ping where size = ConstSize 1 | ||
94 | instance Sized Pong where size = ConstSize 1 | ||
95 | |||
96 | parseDHTAddr :: TransportCrypto -> (ByteString, SockAddr) -> IO (Either (DHTMessage Encrypted8,NodeInfo) (ByteString,SockAddr)) | ||
97 | parseDHTAddr 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 | |||
120 | encodeDHTAddr :: (DHTMessage Encrypted8,NodeInfo) -> IO (ByteString, SockAddr) | ||
121 | encodeDHTAddr (msg,ni) = return (runPut $ putMessage msg, nodeAddr ni) | ||
122 | |||
123 | dhtMessageType :: ( 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) | ||
128 | dhtMessageType (DHTPing a) = (0x00, putAsymm a) | ||
129 | dhtMessageType (DHTPong a) = (0x01, putAsymm a) | ||
130 | dhtMessageType (DHTGetNodes a) = (0x02, putAsymm a) | ||
131 | dhtMessageType (DHTSendNodes a) = (0x04, putAsymm a) | ||
132 | dhtMessageType (DHTCookieRequest a) = (0x18, putAsymm a) | ||
133 | dhtMessageType (DHTCookie n x) = (0x19, put n >> put x) | ||
134 | dhtMessageType (DHTDHTRequest k a) = (0x20, putPublicKey k >> putAsymm a) | ||
135 | dhtMessageType (DHTLanDiscovery nid) = (0x21, put nid) | ||
136 | |||
137 | putMessage :: DHTMessage Encrypted8 -> Put | ||
138 | putMessage msg = case dhtMessageType msg of (typ,p) -> put typ >> p | ||
139 | |||
140 | getCookie :: Get (Nonce24, Encrypted8 Cookie) | ||
141 | getCookie = get | ||
142 | |||
143 | getDHTReqest :: Get (PublicKey, Asymm (Encrypted8 DHTRequest)) | ||
144 | getDHTReqest = (,) <$> 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 | |||
155 | getDHT :: Sized a => Get (Asymm (Encrypted8 a)) | ||
156 | getDHT = getAsymm | ||
157 | |||
158 | |||
159 | -- Throws an error if called with a non-internet socket. | ||
160 | direct :: Sized a => ByteString | ||
161 | -> SockAddr | ||
162 | -> (Asymm (Encrypted8 a) -> DHTMessage Encrypted8) | ||
163 | -> Either String (DHTMessage Encrypted8, NodeInfo) | ||
164 | direct bs saddr f = fanGet bs getDHT f (asymNodeInfo saddr) | ||
165 | |||
166 | -- Throws an error if called with a non-internet socket. | ||
167 | asymNodeInfo :: SockAddr -> Asymm a -> NodeInfo | ||
168 | asymNodeInfo saddr asym = either (error . mappend "asymNodeInfo: ") id $ nodeInfo (key2id $ senderKey asym) saddr | ||
169 | |||
170 | |||
171 | fanGet :: ByteString -> Get x -> (x -> a) -> (x -> b) -> Either String (a,b) | ||
172 | fanGet bs getIt f nid = fmap (f &&& nid) $ runGet getIt bs | ||
173 | |||
174 | -- Throws an error if called with a non-internet socket. | ||
175 | noReplyAddr :: SockAddr -> NodeInfo | ||
176 | noReplyAddr saddr = either (error . mappend "noReplyAddr: ") id $ nodeInfo zeroID saddr | ||
177 | |||
178 | |||
179 | data 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 | |||
209 | instance 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 | |||
219 | instance 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 | | ||
243 | data 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] | ||
257 | data FriendRequest = FriendRequest | ||
258 | { friendNoSpam :: Word32 | ||
259 | , friendRequestText :: ByteString -- UTF8 | ||
260 | } | ||
261 | deriving (Eq, Show) | ||
262 | |||
263 | data NoSpam = NoSpam !Word32 !(Maybe Word16) | ||
264 | |||
265 | instance 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 | |||
271 | base64decode :: Monad m => t1 -> Get t -> String -> m (t, t1) | ||
272 | base64decode rs getter s = | ||
273 | either fail (\a -> return (a,rs)) | ||
274 | $ runGet getter | ||
275 | =<< Base64.decode (B8.pack s) | ||
276 | |||
277 | base16decode :: Monad m => t1 -> Get t -> String -> m (t, t1) | ||
278 | base16decode rs getter s = | ||
279 | either fail (\a -> return (a,rs)) | ||
280 | $ runGet getter | ||
281 | $ fst | ||
282 | $ Base16.decode (B8.pack s) | ||
283 | |||
284 | verifyChecksum :: PublicKey -> Word16 -> Either String () | ||
285 | verifyChecksum _ _ = 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 | ||
297 | data LongTermKeyWrap = LongTermKeyWrap | ||
298 | { wrapLongTermKey :: PublicKey | ||
299 | , wrapNonce :: Nonce24 | ||
300 | , wrapData :: Encrypted DHTPublicKey | ||
301 | } | ||
302 | deriving Show | ||
303 | |||
304 | instance Serialize LongTermKeyWrap where | ||
305 | get = LongTermKeyWrap <$> getPublicKey <*> get <*> get | ||
306 | put (LongTermKeyWrap key nonce dta) = putPublicKey key >> put nonce >> put dta | ||
307 | |||
308 | |||
309 | instance 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 | |||
316 | instance 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. | ||
320 | instance Sized FriendRequest where | ||
321 | size = contramap friendNoSpam size <> contramap friendRequestText (VarSize B.length) | ||
322 | |||
323 | instance 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 | |||
331 | instance Serialize FriendRequest where | ||
332 | get = FriendRequest <$> get <*> (remaining >>= getBytes) | ||
333 | put (FriendRequest nospam txt) = put nospam >> putByteString txt | ||
334 | |||
335 | newtype GetNodes = GetNodes NodeId | ||
336 | deriving (Eq,Ord,Show,Read,S.Serialize) | ||
337 | |||
338 | instance Sized GetNodes where | ||
339 | size = ConstSize 32 -- TODO This right? | ||
340 | |||
341 | newtype SendNodes = SendNodes [NodeInfo] | ||
342 | deriving (Eq,Ord,Show,Read) | ||
343 | |||
344 | instance 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 | |||
349 | instance 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 | |||
360 | data Ping = Ping deriving Show | ||
361 | data Pong = Pong deriving Show | ||
362 | |||
363 | instance 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 | |||
370 | instance 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 | |||
377 | newtype CookieRequest = CookieRequest PublicKey | ||
378 | deriving (Eq, Show) | ||
379 | newtype CookieResponse = CookieResponse Cookie | ||
380 | deriving (Eq, Show) | ||
381 | |||
382 | data Cookie = Cookie Nonce24 (Encrypted CookieData) | ||
383 | deriving (Eq, Ord, Show) | ||
384 | |||
385 | instance Sized Cookie where size = ConstSize 112 -- 24 byte nonce + 88 byte cookie data | ||
386 | |||
387 | instance Serialize Cookie where | ||
388 | get = Cookie <$> get <*> get | ||
389 | put (Cookie nonce dta) = put nonce >> put dta | ||
390 | |||
391 | data CookieData = CookieData -- 16 (mac) | ||
392 | { cookieTime :: Word64 -- 8 | ||
393 | , longTermKey :: PublicKey -- 32 | ||
394 | , dhtKey :: PublicKey -- + 32 | ||
395 | } -- = 88 bytes when encrypted. | ||
396 | |||
397 | instance Sized CookieData where | ||
398 | size = ConstSize 72 | ||
399 | |||
400 | instance 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 | |||
409 | instance Sized CookieRequest where | ||
410 | size = ConstSize 64 -- 32 byte key + 32 byte padding | ||
411 | |||
412 | instance Serialize CookieRequest where | ||
413 | get = CookieRequest <$> getPublicKey <* {- padding -} getPublicKey | ||
414 | put (CookieRequest k) = putPublicKey k >> {- padding -} putPublicKey k | ||
415 | |||
416 | forwardDHTRequests :: TransportCrypto -> (PublicKey -> IO (Maybe NodeInfo)) -> DHTTransport -> DHTTransport | ||
417 | forwardDHTRequests 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 | |||
428 | encrypt :: TransportCrypto -> DHTMessage ((,) Nonce8) -> NodeInfo -> IO (DHTMessage Encrypted8, NodeInfo) | ||
429 | encrypt 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 | |||
434 | encryptMessage :: Serialize a => | ||
435 | TransportCrypto -> | ||
436 | PublicKey -> | ||
437 | Nonce24 -> Either (Nonce8,a) (Asymm (Nonce8,a)) -> IO (Encrypted8 a) | ||
438 | encryptMessage 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 | |||
443 | decrypt :: TransportCrypto -> DHTMessage Encrypted8 -> NodeInfo -> IO (Either String (DHTMessage ((,) Nonce8), NodeInfo)) | ||
444 | decrypt 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 | |||
449 | decryptMessage :: Serialize x => | ||
450 | TransportCrypto | ||
451 | -> Nonce24 | ||
452 | -> Either (PublicKey, Encrypted8 x) (Asymm (Encrypted8 x)) | ||
453 | -> IO ((Either String ∘ ((,) Nonce8)) x) | ||
454 | decryptMessage 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 | |||
460 | sequenceMessage :: Applicative m => DHTMessage (m ∘ f) -> m (DHTMessage f) | ||
461 | sequenceMessage (DHTPing asym) = fmap DHTPing $ sequenceA $ fmap uncomposed asym | ||
462 | sequenceMessage (DHTPong asym) = fmap DHTPong $ sequenceA $ fmap uncomposed asym | ||
463 | sequenceMessage (DHTGetNodes asym) = fmap DHTGetNodes $ sequenceA $ fmap uncomposed asym | ||
464 | sequenceMessage (DHTSendNodes asym) = fmap DHTSendNodes $ sequenceA $ fmap uncomposed asym | ||
465 | sequenceMessage (DHTCookieRequest asym) = fmap DHTCookieRequest $ sequenceA $ fmap uncomposed asym | ||
466 | sequenceMessage (DHTCookie n dta) = fmap (DHTCookie n) $ uncomposed dta | ||
467 | sequenceMessage (DHTDHTRequest pubkey asym) = fmap (DHTDHTRequest pubkey) $ sequenceA $ fmap uncomposed asym | ||
468 | sequenceMessage (DHTLanDiscovery nid) = pure $ DHTLanDiscovery nid | ||
469 | |||
470 | transcode :: forall f g. (forall a. Serialize a => Nonce24 -> Either (f a) (Asymm (f a)) -> g a) -> DHTMessage f -> DHTMessage g | ||
471 | transcode f (DHTPing asym) = DHTPing $ asym { asymmData = f (asymmNonce asym) (Right asym) } | ||
472 | transcode f (DHTPong asym) = DHTPong $ asym { asymmData = f (asymmNonce asym) (Right asym) } | ||
473 | transcode f (DHTGetNodes asym) = DHTGetNodes $ asym { asymmData = f (asymmNonce asym) (Right asym) } | ||
474 | transcode f (DHTSendNodes asym) = DHTSendNodes $ asym { asymmData = f (asymmNonce asym) (Right asym) } | ||
475 | transcode f (DHTCookieRequest asym) = DHTCookieRequest $ asym { asymmData = f (asymmNonce asym) (Right asym) } | ||
476 | transcode f (DHTCookie n dta) = DHTCookie n $ f n $ Left dta | ||
477 | transcode f (DHTDHTRequest pubkey asym) = DHTDHTRequest pubkey $ asym { asymmData = f (asymmNonce asym) (Right asym) } | ||
478 | transcode 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 -} | ||
17 | module 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 | |||
30 | import Control.Applicative | ||
31 | import Control.Arrow | ||
32 | import Control.Monad | ||
33 | import Crypto.Error.Types (CryptoFailable (..), | ||
34 | throwCryptoError) | ||
35 | import Crypto.PubKey.Curve25519 | ||
36 | import qualified Data.Aeson as JSON | ||
37 | ;import Data.Aeson (FromJSON, ToJSON, (.=)) | ||
38 | import Data.Bits.ByteString () | ||
39 | import qualified Data.ByteArray as BA | ||
40 | ;import Data.ByteArray as BA (ByteArrayAccess) | ||
41 | import qualified Data.ByteString as B | ||
42 | ;import Data.ByteString (ByteString) | ||
43 | import qualified Data.ByteString.Base16 as Base16 | ||
44 | import qualified Data.ByteString.Base64 as Base64 | ||
45 | import qualified Data.ByteString.Char8 as C8 | ||
46 | import Data.Char | ||
47 | import Data.Data | ||
48 | import Data.Hashable | ||
49 | import Data.IP | ||
50 | import Data.Serialize as S | ||
51 | import Data.Word | ||
52 | import Foreign.Storable | ||
53 | import GHC.TypeLits | ||
54 | import Network.Address hiding (nodePort) | ||
55 | import System.IO.Unsafe (unsafeDupablePerformIO) | ||
56 | import qualified Text.ParserCombinators.ReadP as RP | ||
57 | import Text.Read | ||
58 | import Data.Bits | ||
59 | import Crypto.Tox | ||
60 | import Foreign.Ptr | ||
61 | import Data.Function | ||
62 | import 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. | ||
69 | unsafeDoIO :: IO a -> a | ||
70 | #if __GLASGOW_HASKELL__ > 704 | ||
71 | unsafeDoIO = unsafeDupablePerformIO | ||
72 | #else | ||
73 | unsafeDoIO = unsafePerformIO | ||
74 | #endif | ||
75 | |||
76 | unpackPublicKey :: ByteArrayAccess bs => bs -> [Word64] | ||
77 | unpackPublicKey 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 | |||
84 | packPublicKey :: BA.ByteArray bs => [Word64] -> bs | ||
85 | packPublicKey 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. | ||
94 | data NodeId = NodeId [Word64] !(Maybe PublicKey) | ||
95 | |||
96 | instance Eq NodeId where | ||
97 | (NodeId ws _) == (NodeId xs _) | ||
98 | = ws == xs | ||
99 | |||
100 | instance Ord NodeId where | ||
101 | compare (NodeId ws _) (NodeId xs _) = compare ws xs | ||
102 | |||
103 | instance Sized NodeId where size = ConstSize 32 | ||
104 | |||
105 | key2id :: PublicKey -> NodeId | ||
106 | key2id k = NodeId (unpackPublicKey k) (Just k) | ||
107 | |||
108 | bs2id :: ByteString -> NodeId | ||
109 | bs2id bs = uncurry NodeId . (unpackPublicKey &&& Just) $ throwCryptoError . publicKey $ bs | ||
110 | |||
111 | id2key :: NodeId -> PublicKey | ||
112 | id2key (NodeId ws (Just key)) = key | ||
113 | id2key (NodeId key Nothing) = throwCryptoError . publicKey $ (packPublicKey key :: BA.Bytes) | ||
114 | |||
115 | zeroKey :: PublicKey | ||
116 | zeroKey = throwCryptoError $ publicKey $ B.replicate 32 0 | ||
117 | |||
118 | zeroID :: NodeId | ||
119 | zeroID = NodeId (replicate 4 0) (Just zeroKey) | ||
120 | |||
121 | -- | Convert to and from a Base64 variant that uses .- instead of +/. | ||
122 | nmtoken64 :: Bool -> Char -> Char | ||
123 | nmtoken64 False '.' = '+' | ||
124 | nmtoken64 False '-' = '/' | ||
125 | nmtoken64 True '+' = '.' | ||
126 | nmtoken64 True '/' = '-' | ||
127 | nmtoken64 _ c = c | ||
128 | |||
129 | instance 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 | |||
136 | instance Show NodeId where | ||
137 | show nid = map (nmtoken64 True) $ C8.unpack $ BA.drop 1 $ Base64.encode $ BA.cons 0 $ BA.convert $ id2key nid | ||
138 | |||
139 | instance S.Serialize NodeId where | ||
140 | get = key2id <$> getPublicKey | ||
141 | put nid = putPublicKey $ id2key nid | ||
142 | |||
143 | instance Hashable NodeId where | ||
144 | hashWithSalt salt (NodeId ws _) = hashWithSalt salt (head ws) | ||
145 | |||
146 | testNodeIdBit :: NodeId -> Word -> Bool | ||
147 | testNodeIdBit (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 | |||
153 | xorNodeId :: NodeId -> NodeId -> NodeId | ||
154 | xorNodeId (NodeId xs _) (NodeId ys _) = NodeId (zipWith xor xs ys) Nothing | ||
155 | |||
156 | sampleNodeId :: Applicative m => (Int -> m ByteString) -> NodeId -> (Int,Word8,Word8) -> m NodeId | ||
157 | sampleNodeId 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 | |||
169 | data NodeInfo = NodeInfo | ||
170 | { nodeId :: NodeId | ||
171 | , nodeIP :: IP | ||
172 | , nodePort :: PortNumber | ||
173 | } | ||
174 | deriving (Eq,Ord) | ||
175 | |||
176 | nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo | ||
177 | nodeInfo 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 | |||
183 | instance 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 | ] | ||
200 | instance 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 | |||
214 | getIP :: Word8 -> S.Get IP | ||
215 | getIP 0x02 = IPv4 <$> S.get | ||
216 | getIP 0x0a = IPv6 <$> S.get | ||
217 | getIP 0x82 = IPv4 <$> S.get -- TODO: TCP | ||
218 | getIP 0x8a = IPv6 <$> S.get -- TODO: TCP | ||
219 | getIP x = fail ("unsupported address family ("++show x++")") | ||
220 | |||
221 | instance 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 | |||
227 | instance 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 | |||
245 | hexdigit :: Char -> Bool | ||
246 | hexdigit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'f') || ( 'A' <= c && c <= 'F') | ||
247 | |||
248 | b64digit :: Char -> Bool | ||
249 | b64digit '.' = True | ||
250 | b64digit '+' = True | ||
251 | b64digit '-' = True | ||
252 | b64digit '/' = True | ||
253 | b64digit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'z') || ( 'A' <= c && c <= 'Z') | ||
254 | |||
255 | instance 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. | ||
283 | instance Hashable NodeInfo where | ||
284 | hashWithSalt s ni = hashWithSalt s (nodeIP ni , nodePort ni) | ||
285 | {-# INLINE hashWithSalt #-} | ||
286 | |||
287 | |||
288 | instance 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 | {- | ||
301 | type NodeId = PubKey | ||
302 | |||
303 | pattern NodeId bs = PubKey bs | ||
304 | |||
305 | -- TODO: This should probably be represented by Curve25519.PublicKey, but | ||
306 | -- ByteString has more instances... | ||
307 | newtype PubKey = PubKey ByteString | ||
308 | deriving (Eq,Ord,Data, ByteArrayAccess, Bits, Hashable) | ||
309 | |||
310 | instance Serialize PubKey where | ||
311 | get = PubKey <$> getBytes 32 | ||
312 | put (PubKey bs) = putByteString bs | ||
313 | |||
314 | instance Show PubKey where | ||
315 | show (PubKey bs) = C8.unpack $ Base16.encode bs | ||
316 | |||
317 | instance FiniteBits PubKey where | ||
318 | finiteBitSize _ = 256 | ||
319 | |||
320 | instance 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 | |||
330 | data NodeInfo = NodeInfo | ||
331 | { nodeId :: NodeId | ||
332 | , nodeIP :: IP | ||
333 | , nodePort :: PortNumber | ||
334 | } | ||
335 | deriving (Eq,Ord,Data) | ||
336 | |||
337 | instance Data PortNumber where | ||
338 | dataTypeOf _ = mkNoRepType "PortNumber" | ||
339 | toConstr _ = error "PortNumber.toConstr" | ||
340 | gunfold _ _ = error "PortNumber.gunfold" | ||
341 | |||
342 | instance 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 | ] | ||
359 | instance 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 | |||
371 | getIP :: Word8 -> S.Get IP | ||
372 | getIP 0x02 = IPv4 <$> S.get | ||
373 | getIP 0x0a = IPv6 <$> S.get | ||
374 | getIP 0x82 = IPv4 <$> S.get -- TODO: TCP | ||
375 | getIP 0x8a = IPv6 <$> S.get -- TODO: TCP | ||
376 | getIP x = fail ("unsupported address family ("++show x++")") | ||
377 | |||
378 | instance 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 | |||
401 | hexdigit :: Char -> Bool | ||
402 | hexdigit c = ('0' <= c && c <= '9') || ( 'a' <= c && c <= 'f') || ( 'A' <= c && c <= 'F') | ||
403 | |||
404 | instance 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. | ||
433 | instance Hashable NodeInfo where | ||
434 | hashWithSalt s ni = hashWithSalt s (nodeIP ni , nodePort ni) | ||
435 | {-# INLINE hashWithSalt #-} | ||
436 | |||
437 | |||
438 | instance 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 | |||
447 | nodeInfo :: NodeId -> SockAddr -> Either String NodeInfo | ||
448 | nodeInfo 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 | |||
453 | zeroID :: NodeId | ||
454 | zeroID = PubKey $ B.replicate 32 0 | ||
455 | |||
456 | -} | ||
457 | |||
458 | nodeAddr :: NodeInfo -> SockAddr | ||
459 | nodeAddr (NodeInfo _ ip port) = setPort port $ toSockAddr ip | ||
460 | |||
461 | |||
462 | newtype ForwardPath (n::Nat) = ForwardPath ByteString | ||
463 | deriving (Eq, Ord,Data) | ||
464 | |||
465 | {- | ||
466 | class KnownNat n => OnionPacket n where | ||
467 | mkOnion :: ReturnPath n -> Packet -> Packet | ||
468 | instance OnionPacket 0 where mkOnion _ = id | ||
469 | instance 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 #-} | ||
3 | module Network.Tox.Onion.Handlers where | ||
4 | |||
5 | import Network.Kademlia.Search | ||
6 | import Network.Tox.DHT.Transport | ||
7 | import Network.Tox.DHT.Handlers hiding (Message,Client) | ||
8 | import Network.Tox.Onion.Transport | ||
9 | import Network.QueryResponse as QR hiding (Client) | ||
10 | import qualified Network.QueryResponse as QR (Client) | ||
11 | import Crypto.Tox | ||
12 | import qualified Data.Wrapper.PSQ as PSQ | ||
13 | ;import Data.Wrapper.PSQ (PSQ) | ||
14 | import Crypto.Error.Types (CryptoFailable (..), | ||
15 | throwCryptoError) | ||
16 | import Control.Arrow | ||
17 | |||
18 | import System.IO | ||
19 | import qualified Data.ByteArray as BA | ||
20 | import Data.Function | ||
21 | import Data.Serialize as S | ||
22 | import qualified Data.Wrapper.PSQInt as Int | ||
23 | import Network.Kademlia | ||
24 | import Network.Address (WantIP (..), ipFamily, testIdBit) | ||
25 | import qualified Network.Kademlia.Routing as R | ||
26 | import Control.TriadCommittee | ||
27 | import qualified Data.MinMaxPSQ as MinMaxPSQ | ||
28 | ;import Data.MinMaxPSQ (MinMaxPSQ') | ||
29 | import Network.BitTorrent.DHT.Token as Token | ||
30 | |||
31 | import Control.Exception hiding (Handler) | ||
32 | import Control.Monad | ||
33 | import Control.Concurrent.STM | ||
34 | import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime) | ||
35 | import Network.Socket | ||
36 | import Data.IP | ||
37 | import Data.Maybe | ||
38 | import Data.Bits | ||
39 | import Data.Ord | ||
40 | import Data.Functor.Identity | ||
41 | |||
42 | type Client r = QR.Client String PacketKind TransactionId (OnionDestination r) Message | ||
43 | type Message = OnionMessage Identity | ||
44 | |||
45 | classify :: Message -> MessageClass String PacketKind TransactionId (OnionDestination r) Message | ||
46 | classify 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. | ||
66 | announceH :: Routing -> TVar SessionTokens -> TVar AnnouncedKeys -> OnionDestination r -> AnnounceRequest -> IO AnnounceResponse | ||
67 | announceH 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 | |||
112 | dataToRouteH :: | ||
113 | TVar AnnouncedKeys | ||
114 | -> Transport err (OnionDestination r) (OnionMessage f) | ||
115 | -> addr | ||
116 | -> OnionMessage f | ||
117 | -> IO () | ||
118 | dataToRouteH 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 | |||
133 | type NodeDistance = NodeId | ||
134 | |||
135 | data AnnouncedRoute = AnnouncedRoute NodeInfo (ReturnPath N3) | ||
136 | |||
137 | toOnionDestination :: AnnouncedRoute -> OnionDestination r | ||
138 | toOnionDestination (AnnouncedRoute ni rpath) = OnionToOwner ni rpath | ||
139 | |||
140 | data AnnouncedKeys = AnnouncedKeys | ||
141 | { keyByAge :: !(PSQ NodeId (Down POSIXTime)) -- TODO: timeout of 300 seconds | ||
142 | , keyAssoc :: !(MinMaxPSQ' NodeId NodeDistance (Int,AnnouncedRoute)) | ||
143 | } | ||
144 | |||
145 | |||
146 | insertKey :: POSIXTime -> NodeId -> AnnouncedRoute -> NodeDistance -> AnnouncedKeys -> AnnouncedKeys | ||
147 | insertKey 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 | |||
154 | areq :: Message -> Either String AnnounceRequest | ||
155 | areq (OnionAnnounce asymm) = Right $ fst $ runIdentity $ asymmData asymm | ||
156 | areq _ = Left "Unexpected non-announce OnionMessage" | ||
157 | |||
158 | handlers :: Transport err (OnionDestination r) Message | ||
159 | -> Routing | ||
160 | -> TVar SessionTokens | ||
161 | -> TVar AnnouncedKeys | ||
162 | -> PacketKind | ||
163 | -> Maybe (MethodHandler String TransactionId (OnionDestination r) Message) | ||
164 | handlers net routing toks keydb AnnounceType | ||
165 | = Just | ||
166 | $ MethodHandler areq (\(TransactionId n8 n24) src dst -> OnionAnnounceResponse n8 n24 . Identity) | ||
167 | $ announceH routing toks keydb | ||
168 | handlers net _ _ keydb _ = Just $ NoReply Right $ dataToRouteH keydb net | ||
169 | |||
170 | |||
171 | toxidSearch :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int)) | ||
172 | -> TransportCrypto | ||
173 | -> Client r | ||
174 | -> Search NodeId (IP, PortNumber) Nonce32 NodeInfo Rendezvous | ||
175 | toxidSearch getTimeout crypto client = Search | ||
176 | { searchSpace = toxSpace | ||
177 | , searchNodeAddress = nodeIP &&& nodePort | ||
178 | , searchQuery = getRendezvous getTimeout crypto client | ||
179 | } | ||
180 | |||
181 | announceSerializer :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int)) | ||
182 | -> MethodSerializer | ||
183 | TransactionId | ||
184 | (OnionDestination r) | ||
185 | (OnionMessage Identity) | ||
186 | PacketKind | ||
187 | AnnounceRequest | ||
188 | (Maybe AnnounceResponse) | ||
189 | announceSerializer 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 | |||
207 | unwrapAnnounceResponse :: Maybe NodeId -> NodeInfo -> AnnounceResponse -> ([NodeInfo], [Rendezvous], Maybe Nonce32) | ||
208 | unwrapAnnounceResponse 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 | |||
233 | sendOnion :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int)) | ||
234 | -> Client r | ||
235 | -> AnnounceRequest | ||
236 | -> OnionDestination r | ||
237 | -> (NodeInfo -> AnnounceResponse -> t) | ||
238 | -> IO (Maybe t) | ||
239 | sendOnion 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. | ||
248 | getRendezvous :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int)) | ||
249 | -> TransportCrypto | ||
250 | -> Client r | ||
251 | -> NodeId | ||
252 | -> NodeInfo | ||
253 | -> IO (Maybe ([NodeInfo],[Rendezvous],Maybe Nonce32)) | ||
254 | getRendezvous 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 | |||
265 | putRendezvous :: (TransactionId -> OnionDestination r -> STM (OnionDestination r, Int)) | ||
266 | -> TransportCrypto | ||
267 | -> Client r | ||
268 | -> PublicKey | ||
269 | -> Nonce32 | ||
270 | -> NodeInfo | ||
271 | -> IO (Maybe (Rendezvous, AnnounceResponse)) | ||
272 | putRendezvous 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 #-} | ||
17 | module 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 | |||
51 | import Network.Address (fromSockAddr,toSockAddr,setPort,either4or6,sockAddrPort) | ||
52 | import Network.QueryResponse | ||
53 | import Crypto.Tox hiding (encrypt,decrypt) | ||
54 | import Network.Tox.NodeId | ||
55 | import qualified Crypto.Tox as ToxCrypto | ||
56 | import Network.Tox.DHT.Transport (NodeInfo(..),NodeId(..),SendNodes(..),nodeInfo,DHTPublicKey(..),FriendRequest,asymNodeInfo) | ||
57 | |||
58 | import Control.Applicative | ||
59 | import Control.Arrow | ||
60 | import Control.Concurrent.STM | ||
61 | import Control.Monad | ||
62 | import qualified Data.ByteString as B | ||
63 | ;import Data.ByteString (ByteString) | ||
64 | import Data.Coerce | ||
65 | import Data.Function | ||
66 | import Data.Functor.Contravariant | ||
67 | import Data.Functor.Identity | ||
68 | import Data.IP | ||
69 | import Data.Maybe | ||
70 | import Data.Monoid | ||
71 | import Data.Serialize as S | ||
72 | import Data.Type.Equality | ||
73 | import Data.Typeable | ||
74 | import Data.Word | ||
75 | import Debug.Trace | ||
76 | import GHC.Generics () | ||
77 | import GHC.TypeLits | ||
78 | import Network.Socket | ||
79 | import System.IO | ||
80 | import qualified Text.ParserCombinators.ReadP as RP | ||
81 | import Data.Hashable | ||
82 | |||
83 | type HandleLo a = Maybe (Either String (ByteString, SockAddr)) -> IO a | ||
84 | |||
85 | type UDPTransport = Transport String SockAddr ByteString | ||
86 | |||
87 | |||
88 | getOnionAsymm :: Get (Asymm (Encrypted DataToRoute)) | ||
89 | getOnionAsymm = getAliasedAsymm | ||
90 | |||
91 | putOnionAsymm :: Serialize a => Word8 -> Put -> Asymm a -> Put | ||
92 | putOnionAsymm typ p a = put typ >> p >> putAliasedAsymm a | ||
93 | |||
94 | data 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 | |||
100 | deriving instance ( Show (f (AnnounceRequest, Nonce8)) | ||
101 | , Show (f AnnounceResponse) | ||
102 | , Show (f DataToRoute) | ||
103 | ) => Show (OnionMessage f) | ||
104 | |||
105 | msgNonce :: OnionMessage f -> Nonce24 | ||
106 | msgNonce (OnionAnnounce a) = asymmNonce a | ||
107 | msgNonce (OnionAnnounceResponse _ n24 _) = n24 | ||
108 | msgNonce (OnionToRoute _ a) = asymmNonce a | ||
109 | msgNonce (OnionToRouteResponse a) = asymmNonce a | ||
110 | |||
111 | data AliasSelector = SearchingAlias | AnnouncingAlias SecretKey PublicKey | ||
112 | deriving (Eq,Show) | ||
113 | |||
114 | data 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 | |||
126 | onionAliasSelector :: OnionDestination r -> AliasSelector | ||
127 | onionAliasSelector (OnionToOwner {} ) = SearchingAlias | ||
128 | onionAliasSelector (OnionDestination{onionAliasSelector' = sel}) = sel | ||
129 | |||
130 | onionKey :: OnionDestination r -> PublicKey | ||
131 | onionKey od = id2key . nodeId $ onionNodeInfo od | ||
132 | |||
133 | instance 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 | |||
144 | instance 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 | |||
157 | onionToOwner :: Asymm a -> ReturnPath N3 -> SockAddr -> Either String (OnionDestination r) | ||
158 | onionToOwner 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 | |||
164 | onion :: Sized msg => | ||
165 | ByteString | ||
166 | -> SockAddr | ||
167 | -> Get (Asymm (Encrypted msg) -> t) | ||
168 | -> Either String (t, OnionDestination r) | ||
169 | onion bs saddr getf = do (f,(asymm,ret3)) <- runGet ((,) <$> getf <*> getOnionRequest) bs | ||
170 | oaddr <- onionToOwner asymm ret3 saddr | ||
171 | return (f asymm, oaddr) | ||
172 | |||
173 | parseOnionAddr :: (SockAddr -> Nonce8 -> IO (Maybe (OnionDestination r))) | ||
174 | -> (ByteString, SockAddr) | ||
175 | -> IO (Either (OnionMessage Encrypted,OnionDestination r) | ||
176 | (ByteString,SockAddr)) | ||
177 | parseOnionAddr 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 | |||
195 | getOnionReply :: Word8 -> Maybe (Get (OnionMessage Encrypted)) | ||
196 | getOnionReply 0x84 = Just $ OnionAnnounceResponse <$> get <*> get <*> get | ||
197 | getOnionReply 0x86 = Just $ OnionToRouteResponse <$> getOnionAsymm | ||
198 | getOnionReply _ = Nothing | ||
199 | |||
200 | putOnionMsg :: OnionMessage Encrypted -> Put | ||
201 | putOnionMsg (OnionAnnounce a) = putOnionAsymm 0x83 (return ()) a | ||
202 | putOnionMsg (OnionToRoute pubkey a) = putOnionAsymm 0x85 (putPublicKey pubkey) a | ||
203 | putOnionMsg (OnionAnnounceResponse n8 n24 x) = put (0x84 :: Word8) >> put n8 >> put n24 >> put x | ||
204 | putOnionMsg (OnionToRouteResponse a) = putOnionAsymm 0x86 (return ()) a | ||
205 | |||
206 | newtype 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. | ||
230 | routeId :: NodeId -> RouteId | ||
231 | routeId nid = RouteId $ mod (hash nid) 12 | ||
232 | |||
233 | |||
234 | encodeOnionAddr :: TransportCrypto | ||
235 | -> (NodeInfo -> RouteId -> IO (Maybe OnionRoute)) | ||
236 | -> (OnionMessage Encrypted,OnionDestination RouteId) | ||
237 | -> IO (Maybe (ByteString, SockAddr)) | ||
238 | encodeOnionAddr crypto _ (msg,OnionToOwner ni p) = | ||
239 | return $ Just ( runPut $ putResponse (OnionResponse p msg) | ||
240 | , nodeAddr ni ) | ||
241 | encodeOnionAddr 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 | ||
245 | encodeOnionAddr 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 | |||
258 | forwardOnions :: TransportCrypto -> UDPTransport -> UDPTransport | ||
259 | forwardOnions crypto udp = udp { awaitMessage = forwardAwait crypto udp } | ||
260 | |||
261 | forwardAwait :: TransportCrypto -> UDPTransport -> HandleLo a -> IO a | ||
262 | forwardAwait 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 | |||
275 | forward :: forall c b b1. (Serialize b, Show b) => | ||
276 | (Maybe (Either String b1) -> c) -> ByteString -> (b -> c) -> c | ||
277 | forward kont bs f = either (kont . Just . Left) f $ decode $ B.tail bs | ||
278 | |||
279 | class SumToThree a b | ||
280 | |||
281 | instance SumToThree N0 N3 | ||
282 | instance SumToThree (S a) b => SumToThree a (S b) | ||
283 | |||
284 | class ( 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 | |||
290 | instance LessThanThree N0 | ||
291 | instance LessThanThree N1 | ||
292 | instance LessThanThree N2 | ||
293 | |||
294 | type 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 | ||
301 | data OnionRequest n = OnionRequest | ||
302 | { onionNonce :: Nonce24 | ||
303 | , onionForward :: Forwarding (ThreeMinus n) (OnionMessage Encrypted) | ||
304 | , pathFromOwner :: ReturnPath n | ||
305 | } | ||
306 | |||
307 | deriving instance ( Show (Forwarding (ThreeMinus n) (OnionMessage Encrypted)) | ||
308 | , KnownNat (PeanoNat n) | ||
309 | ) => Show (OnionRequest n) | ||
310 | |||
311 | instance ( 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 | |||
334 | data OnionResponse n = OnionResponse | ||
335 | { pathToOwner :: ReturnPath n | ||
336 | , msgToOwner :: OnionMessage Encrypted | ||
337 | } | ||
338 | |||
339 | deriving instance KnownNat (PeanoNat n) => Show (OnionResponse n) | ||
340 | |||
341 | instance ( 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 | |||
347 | data Addressed a = Addressed { sockAddr :: SockAddr, unaddressed :: a } | ||
348 | deriving (Eq,Show) | ||
349 | |||
350 | instance 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 | |||
355 | getForwardAddr :: S.Get SockAddr | ||
356 | getForwardAddr = 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 | |||
365 | putForwardAddr :: SockAddr -> S.Put | ||
366 | putForwardAddr 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 | |||
375 | instance Serialize a => Serialize (Addressed a) where | ||
376 | get = Addressed <$> getForwardAddr <*> get | ||
377 | put (Addressed addr x) = putForwardAddr addr >> put x | ||
378 | |||
379 | data N0 | ||
380 | data S n | ||
381 | type N1 = S N0 | ||
382 | type N2 = S N1 | ||
383 | type N3 = S N2 | ||
384 | |||
385 | class KnownPeanoNat n where | ||
386 | peanoVal :: p n -> Int | ||
387 | |||
388 | instance KnownPeanoNat N0 where | ||
389 | peanoVal _ = 0 | ||
390 | instance KnownPeanoNat n => KnownPeanoNat (S n) where | ||
391 | peanoVal _ = 1 + peanoVal (Proxy :: Proxy n) | ||
392 | |||
393 | type family PeanoNat p where | ||
394 | PeanoNat N0 = 0 | ||
395 | PeanoNat (S n) = 1 + PeanoNat n | ||
396 | |||
397 | data 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) | ||
402 | instance Sized (ReturnPath N0) where size = ConstSize 0 | ||
403 | instance 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 | {- | ||
409 | instance KnownNat (PeanoNat n) => Sized (ReturnPath n) where | ||
410 | size = ConstSize $ 59 * fromIntegral (natVal (Proxy :: Proxy (PeanoNat n))) | ||
411 | -} | ||
412 | |||
413 | instance Serialize (ReturnPath N0) where get = pure NoReturnPath | ||
414 | put NoReturnPath = pure () | ||
415 | |||
416 | instance Serialize (ReturnPath N1) where | ||
417 | get = ReturnPath <$> get <*> get | ||
418 | put (ReturnPath n24 p) = put n24 >> put p | ||
419 | |||
420 | instance (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) | ||
427 | instance (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 | |||
432 | instance 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 | |||
442 | data Forwarding n msg where | ||
443 | NotForwarded :: msg -> Forwarding N0 msg | ||
444 | Forwarding :: PublicKey -> Encrypted (Addressed (Forwarding n msg)) -> Forwarding (S n) msg | ||
445 | |||
446 | instance Show msg => Show (Forwarding N0 msg) where | ||
447 | show (NotForwarded x) = "NotForwarded "++show x | ||
448 | |||
449 | instance ( 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 | |||
458 | instance 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 | |||
463 | instance 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 | |||
468 | instance Serialize msg => Serialize (Forwarding N0 msg) where | ||
469 | get = NotForwarded <$> get | ||
470 | put (NotForwarded msg) = put msg | ||
471 | |||
472 | instance (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 | |||
476 | handleOnionRequest :: 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 | ||
482 | handleOnionRequest 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 | |||
498 | wrapSymmetric :: Serialize (ReturnPath n) => | ||
499 | SymmetricKey -> Nonce24 -> SockAddr -> ReturnPath n -> ReturnPath (S n) | ||
500 | wrapSymmetric sym n saddr rpath = ReturnPath n $ encryptSymmetric sym n (encodePlain $ Addressed saddr rpath) | ||
501 | |||
502 | peelSymmetric :: Serialize (Addressed (ReturnPath n)) | ||
503 | => SymmetricKey -> ReturnPath (S n) -> Either String (Addressed (ReturnPath n)) | ||
504 | peelSymmetric sym (ReturnPath nonce e) = decryptSymmetric sym nonce e >>= decodePlain | ||
505 | |||
506 | |||
507 | peelOnion :: Serialize (Addressed (Forwarding n t)) | ||
508 | => TransportCrypto | ||
509 | -> Nonce24 | ||
510 | -> Forwarding (S n) t | ||
511 | -> IO (Either String (Addressed (Forwarding n t))) | ||
512 | peelOnion crypto nonce (Forwarding k fwd) = do | ||
513 | fmap runIdentity . uncomposed <$> decryptMessage crypto (dhtKey crypto) nonce (Right $ Asymm k nonce fwd) | ||
514 | |||
515 | handleOnionResponse :: (KnownPeanoNat n, Sized (ReturnPath n), Serialize (ReturnPath n)) => proxy (S n) -> TransportCrypto -> SockAddr -> UDPTransport -> IO a -> OnionResponse (S n) -> IO a | ||
516 | handleOnionResponse 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 | |||
529 | data 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 | |||
536 | instance Sized AnnounceRequest where size = ConstSize (32*3) | ||
537 | |||
538 | instance 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 | |||
542 | getOnionRequest :: Sized msg => Get (Asymm (Encrypted msg), ReturnPath N3) | ||
543 | getOnionRequest = 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 | |||
552 | putRequest :: ( KnownPeanoNat n | ||
553 | , Serialize (OnionRequest n) | ||
554 | , Typeable n | ||
555 | ) => OnionRequest n -> Put | ||
556 | putRequest req = do | ||
557 | let tag = 0x80 + fromIntegral (peanoVal req) | ||
558 | when (tag <= 0x82) (putWord8 tag) | ||
559 | put req | ||
560 | |||
561 | putResponse :: (KnownPeanoNat n, Serialize (OnionResponse n)) => OnionResponse n -> Put | ||
562 | putResponse 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 | |||
570 | data KeyRecord = NotStored Nonce32 | ||
571 | | SendBackKey PublicKey | ||
572 | | Acknowledged Nonce32 | ||
573 | deriving Show | ||
574 | |||
575 | instance Sized KeyRecord where size = ConstSize 33 | ||
576 | |||
577 | instance 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 | |||
588 | data AnnounceResponse = AnnounceResponse | ||
589 | { is_stored :: KeyRecord | ||
590 | , announceNodes :: SendNodes | ||
591 | } | ||
592 | deriving Show | ||
593 | |||
594 | instance Sized AnnounceResponse where | ||
595 | size = contramap is_stored size <> contramap announceNodes size | ||
596 | |||
597 | getNodeList :: S.Get [NodeInfo] | ||
598 | getNodeList = do | ||
599 | n <- S.get | ||
600 | (:) n <$> (getNodeList <|> pure []) | ||
601 | |||
602 | instance 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 | |||
606 | data DataToRoute = DataToRoute | ||
607 | { dataFromKey :: PublicKey -- Real public key of sender | ||
608 | , dataToRoute :: Encrypted OnionData -- (Word8,ByteString) -- DHTPK 0x9c | ||
609 | } | ||
610 | |||
611 | instance Sized DataToRoute where | ||
612 | size = ConstSize 32 <> contramap dataToRoute size | ||
613 | |||
614 | instance Serialize DataToRoute where | ||
615 | get = DataToRoute <$> getPublicKey <*> get | ||
616 | put (DataToRoute k dta) = putPublicKey k >> put dta | ||
617 | |||
618 | data 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 | |||
642 | instance 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 | |||
653 | instance 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 | |||
663 | selectKey :: TransportCrypto -> OnionMessage f -> OnionDestination r -> IO (SecretKey, PublicKey) | ||
664 | selectKey crypto _ rpath@(OnionDestination (AnnouncingAlias skey pkey) _ _) | ||
665 | = return (skey, pkey) | ||
666 | selectKey crypto msg rpath = return $ aliasKey crypto rpath | ||
667 | |||
668 | encrypt :: TransportCrypto | ||
669 | -> OnionMessage Identity | ||
670 | -> OnionDestination r | ||
671 | -> IO (OnionMessage Encrypted, OnionDestination r) | ||
672 | encrypt 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 | |||
684 | decrypt :: TransportCrypto -> OnionMessage Encrypted -> OnionDestination r -> IO (Either String (OnionMessage Identity, OnionDestination r)) | ||
685 | decrypt 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 | |||
706 | senderkey :: OnionDestination r -> t -> (PublicKey, t) | ||
707 | senderkey addr e = (onionKey addr, e) | ||
708 | |||
709 | aliasKey :: TransportCrypto -> OnionDestination r -> (SecretKey,PublicKey) | ||
710 | aliasKey crypto (OnionToOwner {}) = (transportSecret &&& transportPublic) crypto | ||
711 | aliasKey crypto (OnionDestination {}) = (onionAliasSecret &&& onionAliasPublic) crypto | ||
712 | |||
713 | dhtKey :: TransportCrypto -> (SecretKey,PublicKey) | ||
714 | dhtKey crypto = (transportSecret &&& transportPublic) crypto | ||
715 | |||
716 | decryptMessage :: Serialize x => | ||
717 | TransportCrypto | ||
718 | -> (SecretKey,PublicKey) | ||
719 | -> Nonce24 | ||
720 | -> Either (PublicKey, Encrypted x) | ||
721 | (Asymm (Encrypted x)) | ||
722 | -> IO ((Either String ∘ Identity) x) | ||
723 | decryptMessage 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 | |||
729 | sequenceMessage :: Applicative m => OnionMessage (m ∘ f) -> m (OnionMessage f) | ||
730 | sequenceMessage (OnionAnnounce a) = fmap OnionAnnounce $ sequenceA $ fmap uncomposed a | ||
731 | sequenceMessage (OnionAnnounceResponse n8 n24 dta) = OnionAnnounceResponse n8 n24 <$> uncomposed dta | ||
732 | sequenceMessage (OnionToRoute pub a) = pure $ OnionToRoute pub a | ||
733 | sequenceMessage (OnionToRouteResponse a) = pure $ OnionToRouteResponse a | ||
734 | -- sequenceMessage (OnionToRouteResponse a) = fmap OnionToRouteResponse $ sequenceA $ fmap uncomposed a | ||
735 | |||
736 | transcode :: forall f g. (forall a. Serialize a => Nonce24 -> Either (f a) (Asymm (f a)) -> g a) -> OnionMessage f -> OnionMessage g | ||
737 | transcode f (OnionAnnounce a) = OnionAnnounce $ a { asymmData = f (asymmNonce a) (Right a) } | ||
738 | transcode f (OnionAnnounceResponse n8 n24 dta) = OnionAnnounceResponse n8 n24 $ f n24 $ Left dta | ||
739 | transcode f (OnionToRoute pub a) = OnionToRoute pub a | ||
740 | transcode f (OnionToRouteResponse a) = OnionToRouteResponse a | ||
741 | -- transcode f (OnionToRouteResponse a) = OnionToRouteResponse $ a { asymmData = f (asymmNonce a) (Right a) } | ||
742 | |||
743 | |||
744 | data OnionRoute = OnionRoute | ||
745 | { routeAliasA :: SecretKey | ||
746 | , routeAliasB :: SecretKey | ||
747 | , routeAliasC :: SecretKey | ||
748 | , routeNodeA :: NodeInfo | ||
749 | , routeNodeB :: NodeInfo | ||
750 | , routeNodeC :: NodeInfo | ||
751 | } | ||
752 | |||
753 | wrapForRoute :: TransportCrypto -> OnionMessage Encrypted -> NodeInfo -> OnionRoute -> IO (OnionRequest N0) | ||
754 | wrapForRoute 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 | |||
776 | wrapOnion :: Serialize (Forwarding n msg) => | ||
777 | TransportCrypto | ||
778 | -> SecretKey | ||
779 | -> Nonce24 | ||
780 | -> PublicKey | ||
781 | -> SockAddr | ||
782 | -> Forwarding n msg | ||
783 | -> IO (Forwarding (S n) msg) | ||
784 | wrapOnion 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 | ||
796 | data Rendezvous = Rendezvous | ||
797 | { rendezvousKey :: PublicKey | ||
798 | , rendezvousNode :: NodeInfo | ||
799 | } | ||
800 | deriving Eq | ||
801 | |||
802 | instance Show Rendezvous where | ||
803 | showsPrec d (Rendezvous k ni) | ||
804 | = showsPrec d (key2id k) | ||
805 | . (':' :) | ||
806 | . showsPrec d ni | ||
807 | |||
808 | data AnnouncedRendezvous = AnnouncedRendezvous | ||
809 | { remoteUserKey :: PublicKey | ||
810 | , rendezvous :: Rendezvous | ||
811 | } | ||
812 | deriving Eq | ||
813 | |||
814 | instance Show AnnouncedRendezvous where | ||
815 | showsPrec d (AnnouncedRendezvous remote rendez) | ||
816 | = showsPrec d (key2id remote) | ||
817 | . (':' :) | ||
818 | . showsPrec d rendez | ||
819 | |||
820 | instance 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 | |||
836 | selectAlias :: TransportCrypto -> NodeId -> STM AliasSelector | ||
837 | selectAlias 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 | |||
845 | parseDataToRoute | ||
846 | :: TransportCrypto | ||
847 | -> (OnionMessage Encrypted,OnionDestination r) | ||
848 | -> IO (Either ((PublicKey,OnionData),AnnouncedRendezvous) (OnionMessage Encrypted, OnionDestination r)) | ||
849 | parseDataToRoute 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 | ||
890 | parseDataToRoute _ msg = return $ Right msg | ||
891 | |||
892 | encodeDataToRoute :: TransportCrypto | ||
893 | -> ((PublicKey,OnionData),AnnouncedRendezvous) | ||
894 | -> IO (Maybe (OnionMessage Encrypted,OnionDestination r)) | ||
895 | encodeDataToRoute 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 #-} | ||
9 | module Network.Tox.Transport (toxTransport, RouteId) where | ||
10 | |||
11 | import Network.QueryResponse | ||
12 | import Crypto.Tox | ||
13 | import Network.Tox.DHT.Transport | ||
14 | import Network.Tox.Onion.Transport | ||
15 | import Network.Tox.Crypto.Transport | ||
16 | import OnionRouter | ||
17 | |||
18 | import Network.Socket | ||
19 | |||
20 | toxTransport :: | ||
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 ) | ||
29 | toxTransport 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 @@ | |||
1 | module Network.UPNP where | ||
2 | |||
3 | import Data.Maybe | ||
4 | import Network.Address (sockAddrPort) | ||
5 | import Network.Socket | ||
6 | import System.Directory | ||
7 | import System.IO | ||
8 | import System.Process as Process | ||
9 | |||
10 | protocols :: SocketType -> [String] | ||
11 | protocols Stream = ["tcp"] | ||
12 | protocols Datagram = ["udp"] | ||
13 | protocols _ = ["udp","tcp"] | ||
14 | |||
15 | upnpc :: FilePath | ||
16 | upnpc = "/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. | ||
20 | requestPorts :: String -- ^ Description stored on router. | ||
21 | -> [(SocketType, SockAddr)] -- ^ Protocol-port pairs to request. | ||
22 | -> IO (Maybe ProcessHandle) | ||
23 | requestPorts 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 @@ | |||
1 | module StaticAssert where | ||
2 | |||
3 | import Network.Socket (htonl) | ||
4 | import Language.Haskell.TH | ||
5 | |||
6 | staticAssert :: Bool -> Q [Dec] | ||
7 | staticAssert cond = case cond of | ||
8 | True -> return [] | ||
9 | False -> fail "staticAssert failed" | ||
10 | |||
11 | isLittleEndian :: Bool | ||
12 | isLittleEndian = 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 @@ | |||
1 | module System.Global6 where | ||
2 | |||
3 | import Control.Monad | ||
4 | import Data.IP | ||
5 | import Data.List | ||
6 | import Data.Maybe | ||
7 | import Network.Socket | ||
8 | import System.Process | ||
9 | import Text.Read | ||
10 | |||
11 | parseIpAddr :: String -> Maybe IPv6 | ||
12 | parseIpAddr 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 | |||
24 | global6 :: IO (Maybe IPv6) | ||
25 | global6 = 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 @@ | |||
1 | module Text.XXD where | ||
2 | |||
3 | import qualified Data.ByteString.Base16 as Base16 | ||
4 | import Data.ByteString (ByteString) | ||
5 | import qualified Data.ByteString as B | ||
6 | import Data.Word | ||
7 | import Data.Bits | ||
8 | import Data.Char | ||
9 | import Text.Printf | ||
10 | |||
11 | nibble :: Word8 -> Char | ||
12 | nibble b = intToDigit (fromIntegral (b .&. 0x0F)) | ||
13 | |||
14 | xxd :: Int -> ByteString -> [String] | ||
15 | xxd offset bs | B.null bs = [] | ||
16 | xxd 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 | {- | ||
23 | main = 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 @@ | |||
1 | tox: Add fallback trials to cookie response in case response is from another address than request. | ||
2 | |||
3 | ui: Online help. | ||
4 | |||
5 | ui: Explicit routing table node deletion. "forget" command. | ||
6 | |||
7 | ui: a - with no arguments would list the currently active recuring publications. | ||
8 | |||
9 | kademlia: Change refresh algorithm to refresh farther away buckets before closer ones. | ||
10 | |||
11 | kademlia: Remove (without replacement) stale routing nodes at some point. | ||
12 | |||
13 | bug: 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 | |||
16 | kademlia: Give different networks a different minimum count to terminate | ||
17 | bootstrap. Imperically, tox4: 6 buckets, tox6: 3 buckets | ||
18 | |||
19 | tox: Don't store ourself in the kademlia buckets. | ||
20 | |||
21 | tox: fallback to https://nodes.tox.chat/json | ||
22 | |||
23 | tox: bootstrap motd query | ||
24 | |||
25 | tox: hardening get-nodes test request. | ||
26 | |||
27 | tox: nat ping | ||
28 | |||
29 | tox: cache diffie-helman secrets | ||
30 | |||
31 | tox: Expire ofline Tox announces. | ||
32 | |||
33 | tox: Chat support. | ||
34 | |||
35 | bt: 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 | |||
39 | bt: Limit peers in get_peers response for UDP packet size limiting (around 1k). | ||
40 | |||
41 | bt: Use LMDB backend for peer store (and nodes too?). | ||
42 | |||
43 | maint: 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 | |||
53 | maint: 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 | |||
3 | wire() { | ||
4 | ip link add $1 \ | ||
5 | type veth \ | ||
6 | peer name $2 | ||
7 | } | ||
8 | |||
9 | set -x | ||
10 | |||
11 | # Not using this... | ||
12 | buildhub() { | ||
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... | ||
53 | buildtaps() { | ||
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 | |||
72 | buildtaps | ||
73 | |||
74 | ip addr add 80.99.99.99 dev ep0 | ||
75 | ip addr add 81.99.99.99 dev ep1 | ||
76 | ip addr add 82.99.99.99 dev ep2 | ||
77 | ip addr add 83.99.99.99 dev ep3 | ||
78 | ip addr add 84.99.99.99 dev ep4 | ||
79 | ip addr add 85.99.99.99 dev ep5 | ||
80 | ip addr add 86.99.99.99 dev ep6 | ||
81 | ip addr add 87.99.99.99 dev ep7 | ||
82 | ip addr add 88.99.99.99 dev ep8 | ||
83 | ip addr add 89.99.99.99 dev ep9 | ||
84 | ip addr add 90.99.99.99 dev epA | ||
85 | ip addr add 91.99.99.99 dev epB | ||
86 | ip addr add 92.99.99.99 dev epC | ||
87 | ip addr add 93.99.99.99 dev epD | ||
88 | ip addr add 94.99.99.99 dev epE | ||
89 | ip 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 | |||
3 | cleanhub() { | ||
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 | |||
25 | cleantaps() | ||
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 | |||
46 | cleantaps | ||
47 | |||
48 | rm -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 | ||
2 | cmd="$@" | ||
3 | for ep in ep? | ||
4 | do | ||
5 | ( cd $ep | ||
6 | $cmd | ||
7 | ) | ||
8 | done | ||
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 | ||
2 | ip=$1 | ||
3 | ip route add 80.99.99.99/32 via $host | ||
4 | ip route add 80.99.99.99/32 via $host | ||
5 | ip route add 81.99.99.99/32 via $host | ||
6 | ip route add 82.99.99.99/32 via $host | ||
7 | ip route add 83.99.99.99/32 via $host | ||
8 | ip route add 84.99.99.99/32 via $host | ||
9 | ip route add 85.99.99.99/32 via $host | ||
10 | ip route add 86.99.99.99/32 via $host | ||
11 | ip route add 87.99.99.99/32 via $host | ||
12 | ip route add 88.99.99.99/32 via $host | ||
13 | ip route add 89.99.99.99/32 via $host | ||
14 | ip route add 90.99.99.99/32 via $host | ||
15 | ip route add 91.99.99.99/32 via $host | ||
16 | ip route add 92.99.99.99/32 via $host | ||
17 | ip route add 93.99.99.99/32 via $host | ||
18 | ip route add 94.99.99.99/32 via $host | ||
19 | ip 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 | |||
5 | mkdir -p ep0 ep1 ep2 ep3 ep4 ep5 ep6 ep7 \ | ||
6 | ep8 ep9 epA epB epC epD epE epF | ||
7 | |||
8 | rm -f window-count | ||
9 | echo detach > detached | ||
10 | |||
11 | screen -S test -s ./screen-shell.sh -c detached | ||
12 | sleep 0.5 | ||
13 | screen -S test -X screen | ||
14 | sleep 0.5 | ||
15 | screen -S test -X screen | ||
16 | sleep 0.5 | ||
17 | screen -S test -X screen | ||
18 | sleep 0.5 | ||
19 | |||
20 | screen -S test -X screen | ||
21 | sleep 0.5 | ||
22 | screen -S test -X screen | ||
23 | sleep 0.5 | ||
24 | screen -S test -X screen | ||
25 | sleep 0.5 | ||
26 | screen -S test -X screen | ||
27 | sleep 0.5 | ||
28 | |||
29 | screen -S test -X screen | ||
30 | sleep 0.5 | ||
31 | screen -S test -X screen | ||
32 | sleep 0.5 | ||
33 | screen -S test -X screen | ||
34 | sleep 0.5 | ||
35 | screen -S test -X screen | ||
36 | sleep 0.5 | ||
37 | |||
38 | screen -S test -X screen | ||
39 | sleep 0.5 | ||
40 | screen -S test -X screen | ||
41 | sleep 0.5 | ||
42 | screen -S test -X screen | ||
43 | sleep 0.5 | ||
44 | screen -S test -X screen | ||
45 | sleep 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 | ||
2 | cmd="$@" | ||
3 | echo "$cmd" > /tmp/screen-exchange | ||
4 | screen -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 | ||
2 | ifaddr() { | ||
3 | ip -o address show dev $1 | sed -n 's#.* inet \([^ /]*\).*#\1#p' | ||
4 | } | ||
5 | touch window-count | ||
6 | count=$(cat window-count) | ||
7 | count=${count:-(-1)} | ||
8 | count=$(( $count + 1 )) | ||
9 | echo $count > window-count | ||
10 | digit=$(printf '%X' $count) | ||
11 | cd ep$digit | ||
12 | port=$(( 33400 + $count )) | ||
13 | pwd | ||
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 | ||
2 | ip -o link | sed 's/\([^:]*:[^:]*:*\).*/\1/' | ||