diff options
Diffstat (limited to 'dht/bittorrent')
78 files changed, 11878 insertions, 0 deletions
diff --git a/dht/bittorrent/README.md b/dht/bittorrent/README.md new file mode 100644 index 00000000..32948896 --- /dev/null +++ b/dht/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/dht/bittorrent/Readme.md b/dht/bittorrent/Readme.md new file mode 100644 index 00000000..e092c3ad --- /dev/null +++ b/dht/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/dht/bittorrent/bench/Main.hs b/dht/bittorrent/bench/Main.hs new file mode 100644 index 00000000..f04485ab --- /dev/null +++ b/dht/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/dht/bittorrent/bench/Throughtput.hs b/dht/bittorrent/bench/Throughtput.hs new file mode 100644 index 00000000..d0404405 --- /dev/null +++ b/dht/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/dht/bittorrent/bench/TorrentFile.hs b/dht/bittorrent/bench/TorrentFile.hs new file mode 100644 index 00000000..e91a9c10 --- /dev/null +++ b/dht/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/dht/bittorrent/bittorrent.cabal b/dht/bittorrent/bittorrent.cabal new file mode 100644 index 00000000..8ec314e7 --- /dev/null +++ b/dht/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/dht/bittorrent/dev/README.md b/dht/bittorrent/dev/README.md new file mode 100644 index 00000000..e2cc51a6 --- /dev/null +++ b/dht/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/dht/bittorrent/dev/add-sources.sh b/dht/bittorrent/dev/add-sources.sh new file mode 100755 index 00000000..e125cade --- /dev/null +++ b/dht/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/dht/bittorrent/dev/bench b/dht/bittorrent/dev/bench new file mode 100755 index 00000000..5d03db3f --- /dev/null +++ b/dht/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/dht/bittorrent/dev/test b/dht/bittorrent/dev/test new file mode 100755 index 00000000..2eb85df2 --- /dev/null +++ b/dht/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/dht/bittorrent/dev/update-dependencies.sh b/dht/bittorrent/dev/update-dependencies.sh new file mode 100755 index 00000000..c83694c3 --- /dev/null +++ b/dht/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/dht/bittorrent/examples/Client.hs b/dht/bittorrent/examples/Client.hs new file mode 100644 index 00000000..26711676 --- /dev/null +++ b/dht/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/dht/bittorrent/examples/FS.hs b/dht/bittorrent/examples/FS.hs new file mode 100644 index 00000000..550d85a7 --- /dev/null +++ b/dht/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/dht/bittorrent/examples/MkTorrent.hs b/dht/bittorrent/examples/MkTorrent.hs new file mode 100644 index 00000000..88a84893 --- /dev/null +++ b/dht/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/dht/bittorrent/res/dapper-dvd-amd64.iso.torrent b/dht/bittorrent/res/dapper-dvd-amd64.iso.torrent new file mode 100644 index 00000000..5713344b --- /dev/null +++ b/dht/bittorrent/res/dapper-dvd-amd64.iso.torrent | |||
Binary files differ | |||
diff --git a/dht/bittorrent/res/pkg.torrent b/dht/bittorrent/res/pkg.torrent new file mode 100644 index 00000000..be89e9e0 --- /dev/null +++ b/dht/bittorrent/res/pkg.torrent | |||
Binary files differ | |||
diff --git a/dht/bittorrent/res/testfile b/dht/bittorrent/res/testfile new file mode 100644 index 00000000..8e984818 --- /dev/null +++ b/dht/bittorrent/res/testfile | |||
Binary files differ | |||
diff --git a/dht/bittorrent/res/testfile.torrent b/dht/bittorrent/res/testfile.torrent new file mode 100644 index 00000000..297f56a2 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent.hs b/dht/bittorrent/src/Network/BitTorrent.hs new file mode 100644 index 00000000..91a58887 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Client.hs b/dht/bittorrent/src/Network/BitTorrent/Client.hs new file mode 100644 index 00000000..c84290dd --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Client/Handle.hs b/dht/bittorrent/src/Network/BitTorrent/Client/Handle.hs new file mode 100644 index 00000000..66baac48 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Client/Types.hs b/dht/bittorrent/src/Network/BitTorrent/Client/Types.hs new file mode 100644 index 00000000..e2ad858f --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Exchange.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange.hs new file mode 100644 index 00000000..143bf090 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs new file mode 100644 index 00000000..1be9f970 --- /dev/null +++ b/dht/bittorrent/src/Network/BitTorrent/Exchange/Bitfield.hs | |||
@@ -0,0 +1,405 @@ | |||
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 selection 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 a restriction: | ||
112 | -- each integer in the set should be within the given interval. The greatest | ||
113 | -- lower bound of the interval must be zero, so intervals may be specified by | ||
114 | -- providing a maximum set size. For example, a bitfield of size 10 might | ||
115 | -- contain only indices in interval [0..9]. | ||
116 | -- | ||
117 | -- By convention, we use the following aliases for Int: | ||
118 | -- | ||
119 | -- [ PieceIx ] an Int member of the Bitfield. | ||
120 | -- | ||
121 | -- [ PieceCount ] maximum set size for a Bitfield. | ||
122 | data Bitfield = Bitfield { | ||
123 | bfSize :: !PieceCount | ||
124 | , bfSet :: !IntSet | ||
125 | } deriving (Show, Read, Eq) | ||
126 | |||
127 | -- Invariants: all elements of bfSet lie in [0..bfSize - 1]; | ||
128 | |||
129 | instance Monoid Bitfield where | ||
130 | {-# SPECIALIZE instance Monoid Bitfield #-} | ||
131 | mempty = haveNone 0 | ||
132 | mappend = union | ||
133 | mconcat = unions | ||
134 | |||
135 | {----------------------------------------------------------------------- | ||
136 | Construction | ||
137 | -----------------------------------------------------------------------} | ||
138 | |||
139 | -- | The empty bitfield of the given size. | ||
140 | haveNone :: PieceCount -> Bitfield | ||
141 | haveNone s = Bitfield s S.empty | ||
142 | |||
143 | -- | The full bitfield containing all piece indices for the given size. | ||
144 | haveAll :: PieceCount -> Bitfield | ||
145 | haveAll s = Bitfield s (S.interval 0 (s - 1)) | ||
146 | |||
147 | -- | Insert the index in the set ignoring out of range indices. | ||
148 | have :: PieceIx -> Bitfield -> Bitfield | ||
149 | have ix Bitfield {..} | ||
150 | | 0 <= ix && ix < bfSize = Bitfield bfSize (S.insert ix bfSet) | ||
151 | | otherwise = Bitfield bfSize bfSet | ||
152 | |||
153 | singleton :: PieceIx -> PieceCount -> Bitfield | ||
154 | singleton ix pc = have ix (haveNone pc) | ||
155 | |||
156 | -- | Assign new size to bitfield. FIXME Normally, size should be only | ||
157 | -- decreased, otherwise exception raised. | ||
158 | adjustSize :: PieceCount -> Bitfield -> Bitfield | ||
159 | adjustSize s Bitfield {..} = Bitfield s bfSet | ||
160 | |||
161 | -- | NOTE: for internal use only | ||
162 | interval :: PieceCount -> PieceIx -> PieceIx -> Bitfield | ||
163 | interval pc a b = Bitfield pc (S.interval a b) | ||
164 | |||
165 | {----------------------------------------------------------------------- | ||
166 | Query | ||
167 | -----------------------------------------------------------------------} | ||
168 | |||
169 | -- | Test if bitifield have no one index: peer do not have anything. | ||
170 | null :: Bitfield -> Bool | ||
171 | null Bitfield {..} = S.null bfSet | ||
172 | |||
173 | -- | Test if bitfield have all pieces. | ||
174 | full :: Bitfield -> Bool | ||
175 | full Bitfield {..} = S.size bfSet == bfSize | ||
176 | |||
177 | -- | Count of peer have pieces. | ||
178 | haveCount :: Bitfield -> PieceCount | ||
179 | haveCount = S.size . bfSet | ||
180 | |||
181 | -- | Total count of pieces and its indices. | ||
182 | totalCount :: Bitfield -> PieceCount | ||
183 | totalCount = bfSize | ||
184 | |||
185 | -- | Ratio of /have/ piece count to the /total/ piece count. | ||
186 | -- | ||
187 | -- > forall bf. 0 <= completeness bf <= 1 | ||
188 | -- | ||
189 | completeness :: Bitfield -> Ratio PieceCount | ||
190 | completeness b = haveCount b % totalCount b | ||
191 | |||
192 | inRange :: PieceIx -> Bitfield -> Bool | ||
193 | inRange ix Bitfield {..} = 0 <= ix && ix < bfSize | ||
194 | |||
195 | member :: PieceIx -> Bitfield -> Bool | ||
196 | member ix bf @ Bitfield {..} | ||
197 | | ix `inRange` bf = ix `S.member` bfSet | ||
198 | | otherwise = False | ||
199 | |||
200 | notMember :: PieceIx -> Bitfield -> Bool | ||
201 | notMember ix bf @ Bitfield {..} | ||
202 | | ix `inRange` bf = ix `S.notMember` bfSet | ||
203 | | otherwise = True | ||
204 | |||
205 | -- | Find first available piece index. | ||
206 | findMin :: Bitfield -> PieceIx | ||
207 | findMin = S.findMin . bfSet | ||
208 | {-# INLINE findMin #-} | ||
209 | |||
210 | -- | Find last available piece index. | ||
211 | findMax :: Bitfield -> PieceIx | ||
212 | findMax = S.findMax . bfSet | ||
213 | {-# INLINE findMax #-} | ||
214 | |||
215 | -- | Check if all pieces from first bitfield present if the second bitfield | ||
216 | isSubsetOf :: Bitfield -> Bitfield -> Bool | ||
217 | isSubsetOf a b = bfSet a `S.isSubsetOf` bfSet b | ||
218 | {-# INLINE isSubsetOf #-} | ||
219 | |||
220 | -- | Resulting bitfield includes only missing pieces. | ||
221 | complement :: Bitfield -> Bitfield | ||
222 | complement Bitfield {..} = Bitfield | ||
223 | { bfSet = uni `S.difference` bfSet | ||
224 | , bfSize = bfSize | ||
225 | } | ||
226 | where | ||
227 | Bitfield _ uni = haveAll bfSize | ||
228 | {-# INLINE complement #-} | ||
229 | |||
230 | {----------------------------------------------------------------------- | ||
231 | -- Availability | ||
232 | -----------------------------------------------------------------------} | ||
233 | |||
234 | -- | Frequencies are needed in piece selection startegies which use | ||
235 | -- availability quantity to find out the optimal next piece index to | ||
236 | -- download. | ||
237 | type Frequency = Int | ||
238 | |||
239 | -- TODO rename to availability | ||
240 | -- | How many times each piece index occur in the given bitfield set. | ||
241 | frequencies :: [Bitfield] -> Vector Frequency | ||
242 | frequencies [] = V.fromList [] | ||
243 | frequencies xs = runST $ do | ||
244 | v <- VM.new size | ||
245 | VM.set v 0 | ||
246 | forM_ xs $ \ Bitfield {..} -> do | ||
247 | forM_ (S.toList bfSet) $ \ x -> do | ||
248 | fr <- VM.read v x | ||
249 | VM.write v x (succ fr) | ||
250 | V.unsafeFreeze v | ||
251 | where | ||
252 | size = maximum (map bfSize xs) | ||
253 | |||
254 | -- TODO it seems like this operation is veeery slow | ||
255 | |||
256 | -- | Find least available piece index. If no piece available return | ||
257 | -- 'Nothing'. | ||
258 | rarest :: [Bitfield] -> Maybe PieceIx | ||
259 | rarest xs | ||
260 | | V.null freqMap = Nothing | ||
261 | | otherwise | ||
262 | = Just $ fst $ V.ifoldr' minIx (0, freqMap V.! 0) freqMap | ||
263 | where | ||
264 | freqMap = frequencies xs | ||
265 | {-# NOINLINE freqMap #-} | ||
266 | |||
267 | minIx :: PieceIx -> Frequency | ||
268 | -> (PieceIx, Frequency) | ||
269 | -> (PieceIx, Frequency) | ||
270 | minIx ix fr acc@(_, fra) | ||
271 | | fr < fra && fr > 0 = (ix, fr) | ||
272 | | otherwise = acc | ||
273 | |||
274 | |||
275 | {----------------------------------------------------------------------- | ||
276 | Combine | ||
277 | -----------------------------------------------------------------------} | ||
278 | |||
279 | insert :: PieceIx -> Bitfield -> Bitfield | ||
280 | insert pix bf @ Bitfield {..} | ||
281 | | 0 <= pix && pix < bfSize = Bitfield | ||
282 | { bfSet = S.insert pix bfSet | ||
283 | , bfSize = bfSize | ||
284 | } | ||
285 | | otherwise = bf | ||
286 | |||
287 | -- | Find indices at least one peer have. | ||
288 | union :: Bitfield -> Bitfield -> Bitfield | ||
289 | union a b = {-# SCC union #-} Bitfield { | ||
290 | bfSize = bfSize a `max` bfSize b | ||
291 | , bfSet = bfSet a `S.union` bfSet b | ||
292 | } | ||
293 | |||
294 | -- | Find indices both peers have. | ||
295 | intersection :: Bitfield -> Bitfield -> Bitfield | ||
296 | intersection a b = {-# SCC intersection #-} Bitfield { | ||
297 | bfSize = bfSize a `min` bfSize b | ||
298 | , bfSet = bfSet a `S.intersection` bfSet b | ||
299 | } | ||
300 | |||
301 | -- | Find indices which have first peer but do not have the second peer. | ||
302 | difference :: Bitfield -> Bitfield -> Bitfield | ||
303 | difference a b = {-# SCC difference #-} Bitfield { | ||
304 | bfSize = bfSize a -- FIXME is it reasonable? | ||
305 | , bfSet = bfSet a `S.difference` bfSet b | ||
306 | } | ||
307 | |||
308 | -- | Find indices the any of the peers have. | ||
309 | unions :: [Bitfield] -> Bitfield | ||
310 | unions = {-# SCC unions #-} foldl' union (haveNone 0) | ||
311 | |||
312 | {----------------------------------------------------------------------- | ||
313 | Serialization | ||
314 | -----------------------------------------------------------------------} | ||
315 | |||
316 | -- | List all /have/ indexes. | ||
317 | toList :: Bitfield -> [PieceIx] | ||
318 | toList Bitfield {..} = S.toList bfSet | ||
319 | |||
320 | -- | Make bitfield from list of /have/ indexes. | ||
321 | fromList :: PieceCount -> [PieceIx] -> Bitfield | ||
322 | fromList s ixs = Bitfield { | ||
323 | bfSize = s | ||
324 | , bfSet = S.splitGT (-1) $ S.splitLT s $ S.fromList ixs | ||
325 | } | ||
326 | |||
327 | -- | Unpack 'Bitfield' from tightly packed bit array. Note resulting | ||
328 | -- size might be more than real bitfield size, use 'adjustSize'. | ||
329 | fromBitmap :: ByteString -> Bitfield | ||
330 | fromBitmap bs = {-# SCC fromBitmap #-} Bitfield { | ||
331 | bfSize = B.length bs * 8 | ||
332 | , bfSet = S.fromByteString bs | ||
333 | } | ||
334 | {-# INLINE fromBitmap #-} | ||
335 | |||
336 | -- | Pack a 'Bitfield' to tightly packed bit array. | ||
337 | toBitmap :: Bitfield -> Lazy.ByteString | ||
338 | toBitmap Bitfield {..} = {-# SCC toBitmap #-} Lazy.fromChunks [intsetBM, alignment] | ||
339 | where | ||
340 | byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1 | ||
341 | alignment = B.replicate (byteSize - B.length intsetBM) 0 | ||
342 | intsetBM = S.toByteString bfSet | ||
343 | |||
344 | {----------------------------------------------------------------------- | ||
345 | -- Piece selection | ||
346 | -----------------------------------------------------------------------} | ||
347 | |||
348 | type Selector = Bitfield -- ^ Indices of client /have/ pieces. | ||
349 | -> Bitfield -- ^ Indices of peer /have/ pieces. | ||
350 | -> [Bitfield] -- ^ Indices of other peers /have/ pieces. | ||
351 | -> Maybe PieceIx -- ^ Zero-based index of piece to request | ||
352 | -- to, if any. | ||
353 | |||
354 | selector :: Selector -- ^ Selector to use at the start. | ||
355 | -> Ratio PieceCount | ||
356 | -> Selector -- ^ Selector to use after the client have | ||
357 | -- the C pieces. | ||
358 | -> Selector -- ^ Selector that changes behaviour based | ||
359 | -- on completeness. | ||
360 | selector start pt ready h a xs = | ||
361 | case strategyClass pt h of | ||
362 | SCBeginning -> start h a xs | ||
363 | SCReady -> ready h a xs | ||
364 | SCEnd -> endGame h a xs | ||
365 | |||
366 | data StartegyClass | ||
367 | = SCBeginning | ||
368 | | SCReady | ||
369 | | SCEnd | ||
370 | deriving (Show, Eq, Ord, Enum, Bounded) | ||
371 | |||
372 | |||
373 | strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass | ||
374 | strategyClass threshold = classify . completeness | ||
375 | where | ||
376 | classify c | ||
377 | | c < threshold = SCBeginning | ||
378 | | c + 1 % numerator c < 1 = SCReady | ||
379 | -- FIXME numerator have is not total count | ||
380 | | otherwise = SCEnd | ||
381 | |||
382 | |||
383 | -- | Select the first available piece. | ||
384 | strictFirst :: Selector | ||
385 | strictFirst h a _ = Just $ findMin (difference a h) | ||
386 | |||
387 | -- | Select the last available piece. | ||
388 | strictLast :: Selector | ||
389 | strictLast h a _ = Just $ findMax (difference a h) | ||
390 | |||
391 | -- | | ||
392 | rarestFirst :: Selector | ||
393 | rarestFirst h a xs = rarest (map (intersection want) xs) | ||
394 | where | ||
395 | want = difference h a | ||
396 | |||
397 | -- | In average random first is faster than rarest first strategy but | ||
398 | -- only if all pieces are available. | ||
399 | randomFirst :: Selector | ||
400 | randomFirst = do | ||
401 | -- randomIO | ||
402 | error "TODO: randomFirst" | ||
403 | |||
404 | endGame :: Selector | ||
405 | endGame = strictLast | ||
diff --git a/dht/bittorrent/src/Network/BitTorrent/Exchange/Block.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Block.hs new file mode 100644 index 00000000..bc9a3d24 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Exchange/Connection.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Connection.hs new file mode 100644 index 00000000..6804d0a2 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Exchange/Download.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Download.hs new file mode 100644 index 00000000..981db2fb --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Exchange/Manager.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Manager.hs new file mode 100644 index 00000000..30a6a607 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Exchange/Message.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Message.hs new file mode 100644 index 00000000..5c096523 --- /dev/null +++ b/dht/bittorrent/src/Network/BitTorrent/Exchange/Message.hs | |||
@@ -0,0 +1,1237 @@ | |||
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 | {-# LANGUAGE CPP #-} | ||
36 | {-# OPTIONS -fno-warn-orphans #-} | ||
37 | module Network.BitTorrent.Exchange.Message | ||
38 | ( -- * Capabilities | ||
39 | Capabilities (..) | ||
40 | , Extension (..) | ||
41 | , Caps | ||
42 | |||
43 | -- * Handshake | ||
44 | , ProtocolName | ||
45 | , Handshake(..) | ||
46 | , defaultHandshake | ||
47 | , handshakeSize | ||
48 | , handshakeMaxSize | ||
49 | , handshakeStats | ||
50 | |||
51 | -- * Stats | ||
52 | , ByteCount | ||
53 | , ByteStats (..) | ||
54 | , byteLength | ||
55 | |||
56 | -- * Messages | ||
57 | , Message (..) | ||
58 | , defaultKeepAliveTimeout | ||
59 | , defaultKeepAliveInterval | ||
60 | , PeerMessage (..) | ||
61 | |||
62 | -- ** Core messages | ||
63 | , StatusUpdate (..) | ||
64 | , Available (..) | ||
65 | , Transfer (..) | ||
66 | , defaultRequestQueueLength | ||
67 | |||
68 | -- ** Fast extension | ||
69 | , FastMessage (..) | ||
70 | |||
71 | -- ** Extension protocol | ||
72 | , ExtendedMessage (..) | ||
73 | |||
74 | -- *** Capabilities | ||
75 | , ExtendedExtension (..) | ||
76 | , ExtendedCaps (..) | ||
77 | |||
78 | -- *** Handshake | ||
79 | , ExtendedHandshake (..) | ||
80 | , defaultQueueLength | ||
81 | , nullExtendedHandshake | ||
82 | |||
83 | -- *** Metadata | ||
84 | , ExtendedMetadata (..) | ||
85 | , metadataPieceSize | ||
86 | , defaultMetadataFactor | ||
87 | , defaultMaxInfoDictSize | ||
88 | , isLastPiece | ||
89 | , isValidPiece | ||
90 | ) where | ||
91 | |||
92 | import Control.Applicative | ||
93 | import Control.Arrow ((&&&), (***)) | ||
94 | import Control.Monad (when) | ||
95 | import Data.Attoparsec.ByteString.Char8 as BS | ||
96 | import Data.BEncode as BE | ||
97 | import Data.BEncode.BDict as BE | ||
98 | import Data.BEncode.Internal as BE (ppBEncode, parser) | ||
99 | import Data.BEncode.Types (BDict) | ||
100 | import Data.Bits | ||
101 | import Data.ByteString as BS | ||
102 | import Data.ByteString.Char8 as BC | ||
103 | import Data.ByteString.Lazy as BL | ||
104 | import Data.Default | ||
105 | import Data.List as L | ||
106 | import Data.Map.Strict as M | ||
107 | import Data.Maybe | ||
108 | import Data.Monoid | ||
109 | import Data.Ord | ||
110 | import Data.Serialize as S | ||
111 | import Data.String | ||
112 | import Data.Text as T | ||
113 | import Data.Typeable | ||
114 | import Data.Word | ||
115 | #if MIN_VERSION_iproute(1,7,4) | ||
116 | import Data.IP hiding (fromSockAddr) | ||
117 | #else | ||
118 | import Data.IP | ||
119 | #endif | ||
120 | import Network | ||
121 | import Network.Socket hiding (KeepAlive) | ||
122 | import Text.PrettyPrint as PP hiding ((<>)) | ||
123 | import Text.PrettyPrint.HughesPJClass hiding ((<>),($$)) | ||
124 | |||
125 | import Data.Torrent hiding (Piece (..)) | ||
126 | import qualified Data.Torrent as P (Piece (..)) | ||
127 | import Network.Address | ||
128 | import Network.BitTorrent.Exchange.Bitfield | ||
129 | import Network.BitTorrent.Exchange.Block | ||
130 | |||
131 | {----------------------------------------------------------------------- | ||
132 | -- Capabilities | ||
133 | -----------------------------------------------------------------------} | ||
134 | |||
135 | -- | | ||
136 | class Capabilities caps where | ||
137 | type Ext caps :: * | ||
138 | |||
139 | -- | Pack extensions to caps. | ||
140 | toCaps :: [Ext caps] -> caps | ||
141 | |||
142 | -- | Unpack extensions from caps. | ||
143 | fromCaps :: caps -> [Ext caps] | ||
144 | |||
145 | -- | Check if an extension is a member of the specified set. | ||
146 | allowed :: Ext caps -> caps -> Bool | ||
147 | |||
148 | ppCaps :: Capabilities caps => Pretty (Ext caps) => caps -> Doc | ||
149 | ppCaps = hcat . punctuate ", " . L.map pPrint . fromCaps | ||
150 | |||
151 | {----------------------------------------------------------------------- | ||
152 | -- Extensions | ||
153 | -----------------------------------------------------------------------} | ||
154 | |||
155 | -- | Enumeration of message extension protocols. | ||
156 | -- | ||
157 | -- For more info see: <http://www.bittorrent.org/beps/bep_0004.html> | ||
158 | -- | ||
159 | data Extension | ||
160 | = ExtDHT -- ^ BEP 5: allow to send PORT messages. | ||
161 | | ExtFast -- ^ BEP 6: allow to send FAST messages. | ||
162 | | ExtExtended -- ^ BEP 10: allow to send the extension protocol messages. | ||
163 | deriving (Show, Eq, Ord, Enum, Bounded) | ||
164 | |||
165 | -- | Full extension names, suitable for logging. | ||
166 | instance Pretty Extension where | ||
167 | pPrint ExtDHT = "Distributed Hash Table Protocol" | ||
168 | pPrint ExtFast = "Fast Extension" | ||
169 | pPrint ExtExtended = "Extension Protocol" | ||
170 | |||
171 | -- | Extension bitmask as specified by BEP 4. | ||
172 | extMask :: Extension -> Word64 | ||
173 | extMask ExtDHT = 0x01 | ||
174 | extMask ExtFast = 0x04 | ||
175 | extMask ExtExtended = 0x100000 | ||
176 | |||
177 | {----------------------------------------------------------------------- | ||
178 | -- Capabilities | ||
179 | -----------------------------------------------------------------------} | ||
180 | |||
181 | -- | Capabilities is a set of 'Extension's usually sent in 'Handshake' | ||
182 | -- messages. | ||
183 | newtype Caps = Caps Word64 | ||
184 | deriving (Show, Eq) | ||
185 | |||
186 | -- | Render set of extensions as comma separated list. | ||
187 | instance Pretty Caps where | ||
188 | pPrint = ppCaps | ||
189 | {-# INLINE pPrint #-} | ||
190 | |||
191 | -- | The empty set. | ||
192 | instance Default Caps where | ||
193 | def = Caps 0 | ||
194 | {-# INLINE def #-} | ||
195 | |||
196 | -- | Monoid under intersection. 'mempty' includes all known extensions. | ||
197 | instance Monoid Caps where | ||
198 | mempty = toCaps [minBound .. maxBound] | ||
199 | {-# INLINE mempty #-} | ||
200 | |||
201 | mappend (Caps a) (Caps b) = Caps (a .&. b) | ||
202 | {-# INLINE mappend #-} | ||
203 | |||
204 | -- | 'Handshake' compatible encoding. | ||
205 | instance Serialize Caps where | ||
206 | put (Caps caps) = S.putWord64be caps | ||
207 | {-# INLINE put #-} | ||
208 | |||
209 | get = Caps <$> S.getWord64be | ||
210 | {-# INLINE get #-} | ||
211 | |||
212 | instance Capabilities Caps where | ||
213 | type Ext Caps = Extension | ||
214 | |||
215 | allowed e (Caps caps) = (extMask e .&. caps) /= 0 | ||
216 | {-# INLINE allowed #-} | ||
217 | |||
218 | toCaps = Caps . L.foldr (.|.) 0 . L.map extMask | ||
219 | fromCaps caps = L.filter (`allowed` caps) [minBound..maxBound] | ||
220 | |||
221 | {----------------------------------------------------------------------- | ||
222 | Handshake | ||
223 | -----------------------------------------------------------------------} | ||
224 | |||
225 | maxProtocolNameSize :: Word8 | ||
226 | maxProtocolNameSize = maxBound | ||
227 | |||
228 | -- | The protocol name is used to identify to the local peer which | ||
229 | -- version of BTP the remote peer uses. | ||
230 | newtype ProtocolName = ProtocolName BS.ByteString | ||
231 | deriving (Eq, Ord, Typeable) | ||
232 | |||
233 | -- | In BTP/1.0 the name is 'BitTorrent protocol'. If this string is | ||
234 | -- different from the local peers own protocol name, then the | ||
235 | -- connection is to be dropped. | ||
236 | instance Default ProtocolName where | ||
237 | def = ProtocolName "BitTorrent protocol" | ||
238 | |||
239 | instance Show ProtocolName where | ||
240 | show (ProtocolName bs) = show bs | ||
241 | |||
242 | instance Pretty ProtocolName where | ||
243 | pPrint (ProtocolName bs) = PP.text $ BC.unpack bs | ||
244 | |||
245 | instance IsString ProtocolName where | ||
246 | fromString str | ||
247 | | L.length str <= fromIntegral maxProtocolNameSize | ||
248 | = ProtocolName (fromString str) | ||
249 | | otherwise = error $ "fromString: ProtocolName too long: " ++ str | ||
250 | |||
251 | instance Serialize ProtocolName where | ||
252 | put (ProtocolName bs) = do | ||
253 | putWord8 $ fromIntegral $ BS.length bs | ||
254 | putByteString bs | ||
255 | |||
256 | get = do | ||
257 | len <- getWord8 | ||
258 | bs <- getByteString $ fromIntegral len | ||
259 | return (ProtocolName bs) | ||
260 | |||
261 | -- | Handshake message is used to exchange all information necessary | ||
262 | -- to establish connection between peers. | ||
263 | -- | ||
264 | data Handshake = Handshake { | ||
265 | -- | Identifier of the protocol. This is usually equal to 'def'. | ||
266 | hsProtocol :: ProtocolName | ||
267 | |||
268 | -- | Reserved bytes used to specify supported BEP's. | ||
269 | , hsReserved :: Caps | ||
270 | |||
271 | -- | Info hash of the info part of the metainfo file. that is | ||
272 | -- transmitted in tracker requests. Info hash of the initiator | ||
273 | -- handshake and response handshake should match, otherwise | ||
274 | -- initiator should break the connection. | ||
275 | -- | ||
276 | , hsInfoHash :: InfoHash | ||
277 | |||
278 | -- | Peer id of the initiator. This is usually the same peer id | ||
279 | -- that is transmitted in tracker requests. | ||
280 | -- | ||
281 | , hsPeerId :: PeerId | ||
282 | |||
283 | } deriving (Show, Eq) | ||
284 | |||
285 | instance Serialize Handshake where | ||
286 | put Handshake {..} = do | ||
287 | put hsProtocol | ||
288 | put hsReserved | ||
289 | put hsInfoHash | ||
290 | put hsPeerId | ||
291 | get = Handshake <$> get <*> get <*> get <*> get | ||
292 | |||
293 | -- | Show handshake protocol string, caps and fingerprint. | ||
294 | instance Pretty Handshake where | ||
295 | pPrint Handshake {..} | ||
296 | = pPrint hsProtocol $$ | ||
297 | pPrint hsReserved $$ | ||
298 | pPrint (fingerprint hsPeerId) | ||
299 | |||
300 | -- | Get handshake message size in bytes from the length of protocol | ||
301 | -- string. | ||
302 | handshakeSize :: Word8 -> Int | ||
303 | handshakeSize n = 1 + fromIntegral n + 8 + 20 + 20 | ||
304 | |||
305 | -- | Maximum size of handshake message in bytes. | ||
306 | handshakeMaxSize :: Int | ||
307 | handshakeMaxSize = handshakeSize maxProtocolNameSize | ||
308 | |||
309 | -- | Handshake with default protocol string and reserved bitmask. | ||
310 | defaultHandshake :: InfoHash -> PeerId -> Handshake | ||
311 | defaultHandshake = Handshake def def | ||
312 | |||
313 | handshakeStats :: Handshake -> ByteStats | ||
314 | handshakeStats (Handshake (ProtocolName bs) _ _ _) | ||
315 | = ByteStats 1 (BS.length bs + 8 + 20 + 20) 0 | ||
316 | |||
317 | {----------------------------------------------------------------------- | ||
318 | -- Stats | ||
319 | -----------------------------------------------------------------------} | ||
320 | |||
321 | -- | Number of bytes. | ||
322 | type ByteCount = Int | ||
323 | |||
324 | -- | Summary of encoded message byte layout can be used to collect | ||
325 | -- stats about message flow in both directions. This data can be | ||
326 | -- retrieved using 'stats' function. | ||
327 | data ByteStats = ByteStats | ||
328 | { -- | Number of bytes used to help encode 'control' and 'payload' | ||
329 | -- bytes: message size, message ID's, etc | ||
330 | overhead :: {-# UNPACK #-} !ByteCount | ||
331 | |||
332 | -- | Number of bytes used to exchange peers state\/options: piece | ||
333 | -- and block indexes, infohash, port numbers, peer ID\/IP, etc. | ||
334 | , control :: {-# UNPACK #-} !ByteCount | ||
335 | |||
336 | -- | Number of payload bytes: torrent data blocks and infodict | ||
337 | -- metadata. | ||
338 | , payload :: {-# UNPACK #-} !ByteCount | ||
339 | } deriving Show | ||
340 | |||
341 | instance Pretty ByteStats where | ||
342 | pPrint s @ ByteStats {..} = fsep | ||
343 | [ PP.int overhead, "overhead" | ||
344 | , PP.int control, "control" | ||
345 | , PP.int payload, "payload" | ||
346 | , "bytes" | ||
347 | ] $+$ fsep | ||
348 | [ PP.int (byteLength s), "total bytes" | ||
349 | ] | ||
350 | |||
351 | -- | Empty byte sequences. | ||
352 | instance Default ByteStats where | ||
353 | def = ByteStats 0 0 0 | ||
354 | |||
355 | -- | Monoid under addition. | ||
356 | instance Monoid ByteStats where | ||
357 | mempty = def | ||
358 | mappend a b = ByteStats | ||
359 | { overhead = overhead a + overhead b | ||
360 | , control = control a + control b | ||
361 | , payload = payload a + payload b | ||
362 | } | ||
363 | |||
364 | -- | Sum of the all byte sequences. | ||
365 | byteLength :: ByteStats -> Int | ||
366 | byteLength ByteStats {..} = overhead + control + payload | ||
367 | |||
368 | {----------------------------------------------------------------------- | ||
369 | -- Regular messages | ||
370 | -----------------------------------------------------------------------} | ||
371 | |||
372 | -- | Messages which can be sent after handshaking. Minimal complete | ||
373 | -- definition: 'envelop'. | ||
374 | class PeerMessage a where | ||
375 | -- | Construct a message to be /sent/. Note that if 'ExtendedCaps' | ||
376 | -- do not contain mapping for this message the default | ||
377 | -- 'ExtendedMessageId' is used. | ||
378 | envelop :: ExtendedCaps -- ^ The /receiver/ extended capabilities; | ||
379 | -> a -- ^ An regular message; | ||
380 | -> Message -- ^ Enveloped message to sent. | ||
381 | |||
382 | -- | Find out the extension this message belong to. Can be used to | ||
383 | -- check if this message is allowed to send\/recv in current | ||
384 | -- session. | ||
385 | requires :: a -> Maybe Extension | ||
386 | requires _ = Nothing | ||
387 | |||
388 | -- | Get sizes of overhead\/control\/payload byte sequences of | ||
389 | -- binary message representation without encoding message to binary | ||
390 | -- bytestring. | ||
391 | -- | ||
392 | -- This function should obey one law: | ||
393 | -- | ||
394 | -- * 'byteLength' ('stats' msg) == 'BL.length' ('encode' msg) | ||
395 | -- | ||
396 | stats :: a -> ByteStats | ||
397 | stats _ = ByteStats 4 0 0 | ||
398 | |||
399 | {----------------------------------------------------------------------- | ||
400 | -- Status messages | ||
401 | -----------------------------------------------------------------------} | ||
402 | |||
403 | -- | Notification that the sender have updated its | ||
404 | -- 'Network.BitTorrent.Exchange.Status.PeerStatus'. | ||
405 | data StatusUpdate | ||
406 | -- | Notification that the sender will not upload data to the | ||
407 | -- receiver until unchoking happen. | ||
408 | = Choking !Bool | ||
409 | |||
410 | -- | Notification that the sender is interested (or not interested) | ||
411 | -- in any of the receiver's data pieces. | ||
412 | | Interested !Bool | ||
413 | deriving (Show, Eq, Ord, Typeable) | ||
414 | |||
415 | instance Pretty StatusUpdate where | ||
416 | pPrint (Choking False) = "not choking" | ||
417 | pPrint (Choking True ) = "choking" | ||
418 | pPrint (Interested False) = "not interested" | ||
419 | pPrint (Interested True ) = "interested" | ||
420 | |||
421 | instance PeerMessage StatusUpdate where | ||
422 | envelop _ = Status | ||
423 | {-# INLINE envelop #-} | ||
424 | |||
425 | stats _ = ByteStats 4 1 0 | ||
426 | {-# INLINE stats #-} | ||
427 | |||
428 | {----------------------------------------------------------------------- | ||
429 | -- Available messages | ||
430 | -----------------------------------------------------------------------} | ||
431 | |||
432 | -- | Messages used to inform receiver which pieces of the torrent | ||
433 | -- sender have. | ||
434 | data Available = | ||
435 | -- | Zero-based index of a piece that has just been successfully | ||
436 | -- downloaded and verified via the hash. | ||
437 | Have ! PieceIx | ||
438 | |||
439 | -- | The bitfield message may only be sent immediately after the | ||
440 | -- handshaking sequence is complete, and before any other message | ||
441 | -- are sent. If client have no pieces then bitfield need not to be | ||
442 | -- sent. | ||
443 | | Bitfield !Bitfield | ||
444 | deriving (Show, Eq) | ||
445 | |||
446 | instance Pretty Available where | ||
447 | pPrint (Have ix ) = "Have" <+> int ix | ||
448 | pPrint (Bitfield _ ) = "Bitfield" | ||
449 | |||
450 | instance PeerMessage Available where | ||
451 | envelop _ = Available | ||
452 | {-# INLINE envelop #-} | ||
453 | |||
454 | stats (Have _) = ByteStats (4 + 1) 4 0 | ||
455 | stats (Bitfield bf) = ByteStats (4 + 1) (q + trailing) 0 | ||
456 | where | ||
457 | trailing = if r == 0 then 0 else 1 | ||
458 | (q, r) = quotRem (totalCount bf) 8 | ||
459 | |||
460 | {----------------------------------------------------------------------- | ||
461 | -- Transfer messages | ||
462 | -----------------------------------------------------------------------} | ||
463 | |||
464 | -- | Messages used to transfer 'Block's. | ||
465 | data Transfer | ||
466 | -- | Request for a particular block. If a client is requested a | ||
467 | -- block that another peer do not have the peer might not answer | ||
468 | -- at all. | ||
469 | = Request ! BlockIx | ||
470 | |||
471 | -- | Response to a request for a block. | ||
472 | | Piece !(Block BL.ByteString) | ||
473 | |||
474 | -- | Used to cancel block requests. It is typically used during | ||
475 | -- "End Game". | ||
476 | | Cancel !BlockIx | ||
477 | deriving (Show, Eq) | ||
478 | |||
479 | instance Pretty Transfer where | ||
480 | pPrint (Request ix ) = "Request" <+> pPrint ix | ||
481 | pPrint (Piece blk) = "Piece" <+> pPrint blk | ||
482 | pPrint (Cancel i ) = "Cancel" <+> pPrint i | ||
483 | |||
484 | instance PeerMessage Transfer where | ||
485 | envelop _ = Transfer | ||
486 | {-# INLINE envelop #-} | ||
487 | |||
488 | stats (Request _ ) = ByteStats (4 + 1) (3 * 4) 0 | ||
489 | stats (Piece p ) = ByteStats (4 + 1) (4 + 4 + blockSize p) 0 | ||
490 | stats (Cancel _ ) = ByteStats (4 + 1) (3 * 4) 0 | ||
491 | |||
492 | -- TODO increase | ||
493 | -- | Max number of pending 'Request's inflight. | ||
494 | defaultRequestQueueLength :: Int | ||
495 | defaultRequestQueueLength = 1 | ||
496 | |||
497 | {----------------------------------------------------------------------- | ||
498 | -- Fast messages | ||
499 | -----------------------------------------------------------------------} | ||
500 | |||
501 | -- | BEP6 messages. | ||
502 | data FastMessage = | ||
503 | -- | If a peer have all pieces it might send the 'HaveAll' message | ||
504 | -- instead of 'Bitfield' message. Used to save bandwidth. | ||
505 | HaveAll | ||
506 | |||
507 | -- | If a peer have no pieces it might send 'HaveNone' message | ||
508 | -- intead of 'Bitfield' message. Used to save bandwidth. | ||
509 | | HaveNone | ||
510 | |||
511 | -- | This is an advisory message meaning "you might like to | ||
512 | -- download this piece." Used to avoid excessive disk seeks and | ||
513 | -- amount of IO. | ||
514 | | SuggestPiece !PieceIx | ||
515 | |||
516 | -- | Notifies a requesting peer that its request will not be | ||
517 | -- satisfied. | ||
518 | | RejectRequest !BlockIx | ||
519 | |||
520 | -- | This is an advisory messsage meaning \"if you ask for this | ||
521 | -- piece, I'll give it to you even if you're choked.\" Used to | ||
522 | -- shorten starting phase. | ||
523 | | AllowedFast !PieceIx | ||
524 | deriving (Show, Eq) | ||
525 | |||
526 | instance Pretty FastMessage where | ||
527 | pPrint (HaveAll ) = "Have all" | ||
528 | pPrint (HaveNone ) = "Have none" | ||
529 | pPrint (SuggestPiece pix) = "Suggest" <+> int pix | ||
530 | pPrint (RejectRequest bix) = "Reject" <+> pPrint bix | ||
531 | pPrint (AllowedFast pix) = "Allowed fast" <+> int pix | ||
532 | |||
533 | instance PeerMessage FastMessage where | ||
534 | envelop _ = Fast | ||
535 | {-# INLINE envelop #-} | ||
536 | |||
537 | requires _ = Just ExtFast | ||
538 | {-# INLINE requires #-} | ||
539 | |||
540 | stats HaveAll = ByteStats 4 1 0 | ||
541 | stats HaveNone = ByteStats 4 1 0 | ||
542 | stats (SuggestPiece _) = ByteStats 5 4 0 | ||
543 | stats (RejectRequest _) = ByteStats 5 12 0 | ||
544 | stats (AllowedFast _) = ByteStats 5 4 0 | ||
545 | |||
546 | {----------------------------------------------------------------------- | ||
547 | -- Extension protocol | ||
548 | -----------------------------------------------------------------------} | ||
549 | |||
550 | {----------------------------------------------------------------------- | ||
551 | -- Extended capabilities | ||
552 | -----------------------------------------------------------------------} | ||
553 | |||
554 | data ExtendedExtension | ||
555 | = ExtMetadata -- ^ BEP 9: Extension for Peers to Send Metadata Files | ||
556 | deriving (Show, Eq, Ord, Enum, Bounded, Typeable) | ||
557 | |||
558 | instance IsString ExtendedExtension where | ||
559 | fromString = fromMaybe (error msg) . fromKey . fromString | ||
560 | where | ||
561 | msg = "fromString: could not parse ExtendedExtension" | ||
562 | |||
563 | instance Pretty ExtendedExtension where | ||
564 | pPrint ExtMetadata = "Extension for Peers to Send Metadata Files" | ||
565 | |||
566 | fromKey :: BKey -> Maybe ExtendedExtension | ||
567 | fromKey "ut_metadata" = Just ExtMetadata | ||
568 | fromKey _ = Nothing | ||
569 | {-# INLINE fromKey #-} | ||
570 | |||
571 | toKey :: ExtendedExtension -> BKey | ||
572 | toKey ExtMetadata = "ut_metadata" | ||
573 | {-# INLINE toKey #-} | ||
574 | |||
575 | type ExtendedMessageId = Word8 | ||
576 | |||
577 | extId :: ExtendedExtension -> ExtendedMessageId | ||
578 | extId ExtMetadata = 1 | ||
579 | {-# INLINE extId #-} | ||
580 | |||
581 | type ExtendedMap = Map ExtendedExtension ExtendedMessageId | ||
582 | |||
583 | -- | The extension IDs must be stored for every peer, because every | ||
584 | -- peer may have different IDs for the same extension. | ||
585 | -- | ||
586 | newtype ExtendedCaps = ExtendedCaps { extendedCaps :: ExtendedMap } | ||
587 | deriving (Show, Eq) | ||
588 | |||
589 | instance Pretty ExtendedCaps where | ||
590 | pPrint = ppCaps | ||
591 | {-# INLINE pPrint #-} | ||
592 | |||
593 | -- | The empty set. | ||
594 | instance Default ExtendedCaps where | ||
595 | def = ExtendedCaps M.empty | ||
596 | |||
597 | -- | Monoid under intersection: | ||
598 | -- | ||
599 | -- * The 'mempty' caps includes all known extensions; | ||
600 | -- | ||
601 | -- * the 'mappend' operation is NOT commutative: it return message | ||
602 | -- id from the first caps for the extensions existing in both caps. | ||
603 | -- | ||
604 | instance Monoid ExtendedCaps where | ||
605 | mempty = toCaps [minBound..maxBound] | ||
606 | mappend (ExtendedCaps a) (ExtendedCaps b) = | ||
607 | ExtendedCaps (M.intersection a b) | ||
608 | |||
609 | appendBDict :: BDict -> ExtendedMap -> ExtendedMap | ||
610 | appendBDict (Cons key val xs) caps | ||
611 | | Just ext <- fromKey key | ||
612 | , Right eid <- fromBEncode val = M.insert ext eid (appendBDict xs caps) | ||
613 | | otherwise = appendBDict xs caps | ||
614 | appendBDict Nil caps = caps | ||
615 | |||
616 | -- | Handshake compatible encoding. | ||
617 | instance BEncode ExtendedCaps where | ||
618 | toBEncode = BDict . BE.fromAscList . L.sortBy (comparing fst) | ||
619 | . L.map (toKey *** toBEncode) . M.toList . extendedCaps | ||
620 | |||
621 | fromBEncode (BDict bd) = pure $ ExtendedCaps $ appendBDict bd M.empty | ||
622 | fromBEncode _ = decodingError "ExtendedCaps" | ||
623 | |||
624 | instance Capabilities ExtendedCaps where | ||
625 | type Ext ExtendedCaps = ExtendedExtension | ||
626 | |||
627 | toCaps = ExtendedCaps . M.fromList . L.map (id &&& extId) | ||
628 | |||
629 | fromCaps = M.keys . extendedCaps | ||
630 | {-# INLINE fromCaps #-} | ||
631 | |||
632 | allowed e (ExtendedCaps caps) = M.member e caps | ||
633 | {-# INLINE allowed #-} | ||
634 | |||
635 | remoteMessageId :: ExtendedExtension -> ExtendedCaps -> ExtendedMessageId | ||
636 | remoteMessageId ext = fromMaybe (extId ext) . M.lookup ext . extendedCaps | ||
637 | |||
638 | {----------------------------------------------------------------------- | ||
639 | -- Extended handshake | ||
640 | -----------------------------------------------------------------------} | ||
641 | |||
642 | -- | This message should be sent immediately after the standard | ||
643 | -- bittorrent handshake to any peer that supports this extension | ||
644 | -- protocol. Extended handshakes can be sent more than once, however | ||
645 | -- an implementation may choose to ignore subsequent handshake | ||
646 | -- messages. | ||
647 | -- | ||
648 | data ExtendedHandshake = ExtendedHandshake | ||
649 | { -- | If this peer has an IPv4 interface, this is the compact | ||
650 | -- representation of that address. | ||
651 | ehsIPv4 :: Maybe HostAddress | ||
652 | |||
653 | -- | If this peer has an IPv6 interface, this is the compact | ||
654 | -- representation of that address. | ||
655 | , ehsIPv6 :: Maybe HostAddress6 | ||
656 | |||
657 | -- | Dictionary of supported extension messages which maps names | ||
658 | -- of extensions to an extended message ID for each extension | ||
659 | -- message. | ||
660 | , ehsCaps :: ExtendedCaps | ||
661 | |||
662 | -- | Size of 'Data.Torrent.InfoDict' in bytes. This field should | ||
663 | -- be added if 'ExtMetadata' is enabled in current session /and/ | ||
664 | -- peer have the torrent file. | ||
665 | , ehsMetadataSize :: Maybe Int | ||
666 | |||
667 | -- | Local TCP /listen/ port. Allows each side to learn about the | ||
668 | -- TCP port number of the other side. | ||
669 | , ehsPort :: Maybe PortNumber | ||
670 | |||
671 | -- | Request queue the number of outstanding 'Request' messages | ||
672 | -- this client supports without dropping any. | ||
673 | , ehsQueueLength :: Maybe Int | ||
674 | |||
675 | -- | Client name and version. | ||
676 | , ehsVersion :: Maybe Text | ||
677 | |||
678 | -- | IP of the remote end | ||
679 | , ehsYourIp :: Maybe IP | ||
680 | } deriving (Show, Eq, Typeable) | ||
681 | |||
682 | extHandshakeId :: ExtendedMessageId | ||
683 | extHandshakeId = 0 | ||
684 | |||
685 | -- | Default 'Request' queue size. | ||
686 | defaultQueueLength :: Int | ||
687 | defaultQueueLength = 1 | ||
688 | |||
689 | -- | All fields are empty. | ||
690 | instance Default ExtendedHandshake where | ||
691 | def = ExtendedHandshake def def def def def def def def | ||
692 | |||
693 | instance Monoid ExtendedHandshake where | ||
694 | mempty = def { ehsCaps = mempty } | ||
695 | mappend old new = ExtendedHandshake { | ||
696 | ehsCaps = ehsCaps old <> ehsCaps new, | ||
697 | ehsIPv4 = ehsIPv4 old `mergeOld` ehsIPv4 new, | ||
698 | ehsIPv6 = ehsIPv6 old `mergeOld` ehsIPv6 new, | ||
699 | ehsMetadataSize = ehsMetadataSize old `mergeNew` ehsMetadataSize new, | ||
700 | ehsPort = ehsPort old `mergeOld` ehsPort new, | ||
701 | ehsQueueLength = ehsQueueLength old `mergeNew` ehsQueueLength new, | ||
702 | ehsVersion = ehsVersion old `mergeOld` ehsVersion new, | ||
703 | ehsYourIp = ehsYourIp old `mergeOld` ehsYourIp new | ||
704 | } | ||
705 | where | ||
706 | mergeOld mold mnew = mold <|> mnew | ||
707 | mergeNew mold mnew = mnew <|> mold | ||
708 | |||
709 | |||
710 | instance BEncode ExtendedHandshake where | ||
711 | toBEncode ExtendedHandshake {..} = toDict $ | ||
712 | "ipv4" .=? (S.encode <$> ehsIPv4) | ||
713 | .: "ipv6" .=? (S.encode <$> ehsIPv6) | ||
714 | .: "m" .=! ehsCaps | ||
715 | .: "metadata_size" .=? ehsMetadataSize | ||
716 | .: "p" .=? ehsPort | ||
717 | .: "reqq" .=? ehsQueueLength | ||
718 | .: "v" .=? ehsVersion | ||
719 | .: "yourip" .=? (runPut <$> either put put <$> toEither <$> ehsYourIp) | ||
720 | .: endDict | ||
721 | where | ||
722 | toEither (IPv4 v4) = Left v4 | ||
723 | toEither (IPv6 v6) = Right v6 | ||
724 | |||
725 | fromBEncode = fromDict $ ExtendedHandshake | ||
726 | <$>? "ipv4" | ||
727 | <*>? "ipv6" | ||
728 | <*>! "m" | ||
729 | <*>? "metadata_size" | ||
730 | <*>? "p" | ||
731 | <*>? "reqq" | ||
732 | <*>? "v" | ||
733 | <*> (opt "yourip" >>= getYourIp) | ||
734 | |||
735 | getYourIp :: Maybe BValue -> BE.Get (Maybe IP) | ||
736 | getYourIp f = | ||
737 | return $ do | ||
738 | BString ip <- f | ||
739 | either (const Nothing) Just $ | ||
740 | case BS.length ip of | ||
741 | 4 -> IPv4 <$> S.decode ip | ||
742 | 16 -> IPv6 <$> S.decode ip | ||
743 | _ -> fail "" | ||
744 | |||
745 | instance Pretty ExtendedHandshake where | ||
746 | pPrint = PP.text . show | ||
747 | |||
748 | -- | NOTE: Approximated 'stats'. | ||
749 | instance PeerMessage ExtendedHandshake where | ||
750 | envelop c = envelop c . EHandshake | ||
751 | {-# INLINE envelop #-} | ||
752 | |||
753 | requires _ = Just ExtExtended | ||
754 | {-# INLINE requires #-} | ||
755 | |||
756 | stats _ = ByteStats (4 + 1 + 1) 100 {- is it ok? -} 0 -- FIXME | ||
757 | {-# INLINE stats #-} | ||
758 | |||
759 | -- | Set default values and the specified 'ExtendedCaps'. | ||
760 | nullExtendedHandshake :: ExtendedCaps -> ExtendedHandshake | ||
761 | nullExtendedHandshake caps = ExtendedHandshake | ||
762 | { ehsIPv4 = Nothing | ||
763 | , ehsIPv6 = Nothing | ||
764 | , ehsCaps = caps | ||
765 | , ehsMetadataSize = Nothing | ||
766 | , ehsPort = Nothing | ||
767 | , ehsQueueLength = Just defaultQueueLength | ||
768 | , ehsVersion = Just $ T.pack $ render $ pPrint libFingerprint | ||
769 | , ehsYourIp = Nothing | ||
770 | } | ||
771 | |||
772 | {----------------------------------------------------------------------- | ||
773 | -- Metadata exchange extension | ||
774 | -----------------------------------------------------------------------} | ||
775 | |||
776 | -- | A peer MUST verify that any piece it sends passes the info-hash | ||
777 | -- verification. i.e. until the peer has the entire metadata, it | ||
778 | -- cannot run SHA-1 to verify that it yields the same hash as the | ||
779 | -- info-hash. | ||
780 | -- | ||
781 | data ExtendedMetadata | ||
782 | -- | This message requests the a specified metadata piece. The | ||
783 | -- response to this message, from a peer supporting the extension, | ||
784 | -- is either a 'MetadataReject' or a 'MetadataData' message. | ||
785 | = MetadataRequest PieceIx | ||
786 | |||
787 | -- | If sender requested a valid 'PieceIx' and receiver have the | ||
788 | -- corresponding piece then receiver should respond with this | ||
789 | -- message. | ||
790 | | MetadataData | ||
791 | { -- | A piece of 'Data.Torrent.InfoDict'. | ||
792 | piece :: P.Piece BS.ByteString | ||
793 | |||
794 | -- | This key has the same semantics as the 'ehsMetadataSize' in | ||
795 | -- the 'ExtendedHandshake' — it is size of the torrent info | ||
796 | -- dict. | ||
797 | , totalSize :: Int | ||
798 | } | ||
799 | |||
800 | -- | Peers that do not have the entire metadata MUST respond with | ||
801 | -- a reject message to any metadata request. | ||
802 | -- | ||
803 | -- Clients MAY implement flood protection by rejecting request | ||
804 | -- messages after a certain number of them have been | ||
805 | -- served. Typically the number of pieces of metadata times a | ||
806 | -- factor. | ||
807 | | MetadataReject PieceIx | ||
808 | |||
809 | -- | Reserved. By specification we should ignore unknown metadata | ||
810 | -- messages. | ||
811 | | MetadataUnknown BValue | ||
812 | deriving (Show, Eq, Typeable) | ||
813 | |||
814 | -- | Extended metadata message id used in 'msg_type_key'. | ||
815 | type MetadataId = Int | ||
816 | |||
817 | msg_type_key, piece_key, total_size_key :: BKey | ||
818 | msg_type_key = "msg_type" | ||
819 | piece_key = "piece" | ||
820 | total_size_key = "total_size" | ||
821 | |||
822 | -- | BEP9 compatible encoding. | ||
823 | instance BEncode ExtendedMetadata where | ||
824 | toBEncode (MetadataRequest pix) = toDict $ | ||
825 | msg_type_key .=! (0 :: MetadataId) | ||
826 | .: piece_key .=! pix | ||
827 | .: endDict | ||
828 | toBEncode (MetadataData (P.Piece pix _) totalSize) = toDict $ | ||
829 | msg_type_key .=! (1 :: MetadataId) | ||
830 | .: piece_key .=! pix | ||
831 | .: total_size_key .=! totalSize | ||
832 | .: endDict | ||
833 | toBEncode (MetadataReject pix) = toDict $ | ||
834 | msg_type_key .=! (2 :: MetadataId) | ||
835 | .: piece_key .=! pix | ||
836 | .: endDict | ||
837 | toBEncode (MetadataUnknown bval) = bval | ||
838 | |||
839 | fromBEncode bval = (`fromDict` bval) $ do | ||
840 | mid <- field $ req msg_type_key | ||
841 | case mid :: MetadataId of | ||
842 | 0 -> MetadataRequest <$>! piece_key | ||
843 | 1 -> metadataData <$>! piece_key <*>! total_size_key | ||
844 | 2 -> MetadataReject <$>! piece_key | ||
845 | _ -> pure (MetadataUnknown bval) | ||
846 | where | ||
847 | metadataData pix s = MetadataData (P.Piece pix BS.empty) s | ||
848 | |||
849 | -- | Piece data bytes are omitted. | ||
850 | instance Pretty ExtendedMetadata where | ||
851 | pPrint (MetadataRequest pix ) = "Request" <+> PP.int pix | ||
852 | pPrint (MetadataData p t) = "Data" <+> pPrint p <+> PP.int t | ||
853 | pPrint (MetadataReject pix ) = "Reject" <+> PP.int pix | ||
854 | pPrint (MetadataUnknown bval ) = "Unknown" <+> ppBEncode bval | ||
855 | |||
856 | -- | NOTE: Approximated 'stats'. | ||
857 | instance PeerMessage ExtendedMetadata where | ||
858 | envelop c = envelop c . EMetadata (remoteMessageId ExtMetadata c) | ||
859 | {-# INLINE envelop #-} | ||
860 | |||
861 | requires _ = Just ExtExtended | ||
862 | {-# INLINE requires #-} | ||
863 | |||
864 | stats (MetadataRequest _) = ByteStats (4 + 1 + 1) {- ~ -} 25 0 | ||
865 | stats (MetadataData p _) = ByteStats (4 + 1 + 1) {- ~ -} 41 $ | ||
866 | BS.length (P.pieceData p) | ||
867 | stats (MetadataReject _) = ByteStats (4 + 1 + 1) {- ~ -} 25 0 | ||
868 | stats (MetadataUnknown _) = ByteStats (4 + 1 + 1) {- ? -} 0 0 | ||
869 | |||
870 | -- | All 'Piece's in 'MetadataData' messages MUST have size equal to | ||
871 | -- this value. The last trailing piece can be shorter. | ||
872 | metadataPieceSize :: PieceSize | ||
873 | metadataPieceSize = 16 * 1024 | ||
874 | |||
875 | isLastPiece :: P.Piece a -> Int -> Bool | ||
876 | isLastPiece P.Piece {..} total = succ pieceIndex == pcnt | ||
877 | where | ||
878 | pcnt = q + if r > 0 then 1 else 0 | ||
879 | (q, r) = quotRem total metadataPieceSize | ||
880 | |||
881 | -- TODO we can check if the piece payload bytestring have appropriate | ||
882 | -- length; otherwise serialization MUST fail. | ||
883 | isValidPiece :: P.Piece BL.ByteString -> Int -> Bool | ||
884 | isValidPiece p @ P.Piece {..} total | ||
885 | | isLastPiece p total = pieceSize p <= metadataPieceSize | ||
886 | | otherwise = pieceSize p == metadataPieceSize | ||
887 | |||
888 | setMetadataPayload :: BS.ByteString -> ExtendedMetadata -> ExtendedMetadata | ||
889 | setMetadataPayload bs (MetadataData (P.Piece pix _) t) = | ||
890 | MetadataData (P.Piece pix bs) t | ||
891 | setMetadataPayload _ msg = msg | ||
892 | |||
893 | getMetadataPayload :: ExtendedMetadata -> Maybe BS.ByteString | ||
894 | getMetadataPayload (MetadataData (P.Piece _ bs) _) = Just bs | ||
895 | getMetadataPayload _ = Nothing | ||
896 | |||
897 | -- | Metadata BDict usually contain only 'msg_type_key', 'piece_key' | ||
898 | -- and 'total_size_key' fields so it normally should take less than | ||
899 | -- 100 bytes. This limit is two order of magnitude larger to be | ||
900 | -- permissive to 'MetadataUnknown' messages. | ||
901 | -- | ||
902 | -- See 'maxMessageSize' for further explanation. | ||
903 | -- | ||
904 | maxMetadataBDictSize :: Int | ||
905 | maxMetadataBDictSize = 16 * 1024 | ||
906 | |||
907 | maxMetadataSize :: Int | ||
908 | maxMetadataSize = maxMetadataBDictSize + metadataPieceSize | ||
909 | |||
910 | -- to make MetadataData constructor fields a little bit prettier we | ||
911 | -- cheat here: first we read empty 'pieceData' from bdict, but then we | ||
912 | -- fill that field with the actual piece data — trailing bytes of | ||
913 | -- the message | ||
914 | getMetadata :: Int -> S.Get ExtendedMetadata | ||
915 | getMetadata len | ||
916 | | len > maxMetadataSize = fail $ parseError "size exceeded limit" | ||
917 | | otherwise = do | ||
918 | bs <- getByteString len | ||
919 | parseRes $ BS.parse BE.parser bs | ||
920 | where | ||
921 | parseError reason = "unable to parse metadata message: " ++ reason | ||
922 | |||
923 | parseRes (BS.Fail _ _ m) = fail $ parseError $ "bdict: " ++ m | ||
924 | parseRes (BS.Partial _) = fail $ parseError "bdict: not enough bytes" | ||
925 | parseRes (BS.Done piece bvalueBS) | ||
926 | | BS.length piece > metadataPieceSize | ||
927 | = fail "infodict piece: size exceeded limit" | ||
928 | | otherwise = do | ||
929 | metadata <- either (fail . parseError) pure $ fromBEncode bvalueBS | ||
930 | return $ setMetadataPayload piece metadata | ||
931 | |||
932 | putMetadata :: ExtendedMetadata -> BL.ByteString | ||
933 | putMetadata msg | ||
934 | | Just bs <- getMetadataPayload msg = BE.encode msg <> BL.fromStrict bs | ||
935 | | otherwise = BE.encode msg | ||
936 | |||
937 | -- | Allows a requesting peer to send 2 'MetadataRequest's for the | ||
938 | -- each piece. | ||
939 | -- | ||
940 | -- See 'Network.BitTorrent.Wire.Options.metadataFactor' for | ||
941 | -- explanation why do we need this limit. | ||
942 | defaultMetadataFactor :: Int | ||
943 | defaultMetadataFactor = 2 | ||
944 | |||
945 | -- | Usually torrent size do not exceed 1MB. This value limit torrent | ||
946 | -- /content/ size to about 8TB. | ||
947 | -- | ||
948 | -- See 'Network.BitTorrent.Wire.Options.maxInfoDictSize' for | ||
949 | -- explanation why do we need this limit. | ||
950 | defaultMaxInfoDictSize :: Int | ||
951 | defaultMaxInfoDictSize = 10 * 1024 * 1024 | ||
952 | |||
953 | {----------------------------------------------------------------------- | ||
954 | -- Extension protocol messages | ||
955 | -----------------------------------------------------------------------} | ||
956 | |||
957 | -- | For more info see <http://www.bittorrent.org/beps/bep_0010.html> | ||
958 | data ExtendedMessage | ||
959 | = EHandshake ExtendedHandshake | ||
960 | | EMetadata ExtendedMessageId ExtendedMetadata | ||
961 | | EUnknown ExtendedMessageId BS.ByteString | ||
962 | deriving (Show, Eq, Typeable) | ||
963 | |||
964 | instance Pretty ExtendedMessage where | ||
965 | pPrint (EHandshake ehs) = pPrint ehs | ||
966 | pPrint (EMetadata _ msg) = "Metadata" <+> pPrint msg | ||
967 | pPrint (EUnknown mid _ ) = "Unknown" <+> PP.text (show mid) | ||
968 | |||
969 | instance PeerMessage ExtendedMessage where | ||
970 | envelop _ = Extended | ||
971 | {-# INLINE envelop #-} | ||
972 | |||
973 | requires _ = Just ExtExtended | ||
974 | {-# INLINE requires #-} | ||
975 | |||
976 | stats (EHandshake hs) = stats hs | ||
977 | stats (EMetadata _ msg) = stats msg | ||
978 | stats (EUnknown _ msg) = ByteStats (4 + 1 + 1) (BS.length msg) 0 | ||
979 | |||
980 | {----------------------------------------------------------------------- | ||
981 | -- The message datatype | ||
982 | -----------------------------------------------------------------------} | ||
983 | |||
984 | type MessageId = Word8 | ||
985 | |||
986 | -- | Messages used in communication between peers. | ||
987 | -- | ||
988 | -- Note: If some extensions are disabled (not present in extension | ||
989 | -- mask) and client receive message used by the disabled | ||
990 | -- extension then the client MUST close the connection. | ||
991 | -- | ||
992 | data Message | ||
993 | -- | Peers may close the TCP connection if they have not received | ||
994 | -- any messages for a given period of time, generally 2 | ||
995 | -- minutes. Thus, the KeepAlive message is sent to keep the | ||
996 | -- connection between two peers alive, if no /other/ message has | ||
997 | -- been sent in a given period of time. | ||
998 | = KeepAlive | ||
999 | | Status !StatusUpdate -- ^ Messages used to update peer status. | ||
1000 | | Available !Available -- ^ Messages used to inform availability. | ||
1001 | | Transfer !Transfer -- ^ Messages used to transfer 'Block's. | ||
1002 | |||
1003 | -- | Peer receiving a handshake indicating the remote peer | ||
1004 | -- supports the 'ExtDHT' should send a 'Port' message. Peers that | ||
1005 | -- receive this message should attempt to ping the node on the | ||
1006 | -- received port and IP address of the remote peer. | ||
1007 | | Port !PortNumber | ||
1008 | | Fast !FastMessage | ||
1009 | | Extended !ExtendedMessage | ||
1010 | deriving (Show, Eq) | ||
1011 | |||
1012 | instance Default Message where | ||
1013 | def = KeepAlive | ||
1014 | {-# INLINE def #-} | ||
1015 | |||
1016 | -- | Payload bytes are omitted. | ||
1017 | instance Pretty Message where | ||
1018 | pPrint (KeepAlive ) = "Keep alive" | ||
1019 | pPrint (Status m) = "Status" <+> pPrint m | ||
1020 | pPrint (Available m) = pPrint m | ||
1021 | pPrint (Transfer m) = pPrint m | ||
1022 | pPrint (Port p) = "Port" <+> int (fromEnum p) | ||
1023 | pPrint (Fast m) = pPrint m | ||
1024 | pPrint (Extended m) = pPrint m | ||
1025 | |||
1026 | instance PeerMessage Message where | ||
1027 | envelop _ = id | ||
1028 | {-# INLINE envelop #-} | ||
1029 | |||
1030 | requires KeepAlive = Nothing | ||
1031 | requires (Status _) = Nothing | ||
1032 | requires (Available _) = Nothing | ||
1033 | requires (Transfer _) = Nothing | ||
1034 | requires (Port _) = Just ExtDHT | ||
1035 | requires (Fast _) = Just ExtFast | ||
1036 | requires (Extended _) = Just ExtExtended | ||
1037 | |||
1038 | stats KeepAlive = ByteStats 4 0 0 | ||
1039 | stats (Status m) = stats m | ||
1040 | stats (Available m) = stats m | ||
1041 | stats (Transfer m) = stats m | ||
1042 | stats (Port _) = ByteStats 5 2 0 | ||
1043 | stats (Fast m) = stats m | ||
1044 | stats (Extended m) = stats m | ||
1045 | |||
1046 | -- | PORT message. | ||
1047 | instance PeerMessage PortNumber where | ||
1048 | envelop _ = Port | ||
1049 | {-# INLINE envelop #-} | ||
1050 | |||
1051 | requires _ = Just ExtDHT | ||
1052 | {-# INLINE requires #-} | ||
1053 | |||
1054 | -- | How long /this/ peer should wait before dropping connection, in | ||
1055 | -- seconds. | ||
1056 | defaultKeepAliveTimeout :: Int | ||
1057 | defaultKeepAliveTimeout = 2 * 60 | ||
1058 | |||
1059 | -- | How often /this/ peer should send 'KeepAlive' messages, in | ||
1060 | -- seconds. | ||
1061 | defaultKeepAliveInterval :: Int | ||
1062 | defaultKeepAliveInterval = 60 | ||
1063 | |||
1064 | getInt :: S.Get Int | ||
1065 | getInt = fromIntegral <$> S.getWord32be | ||
1066 | {-# INLINE getInt #-} | ||
1067 | |||
1068 | putInt :: S.Putter Int | ||
1069 | putInt = S.putWord32be . fromIntegral | ||
1070 | {-# INLINE putInt #-} | ||
1071 | |||
1072 | -- | This limit should protect against "out-of-memory" attacks: if a | ||
1073 | -- malicious peer have sent a long varlength message then receiver can | ||
1074 | -- accumulate too long bytestring in the 'Get'. | ||
1075 | -- | ||
1076 | -- Normal messages should never exceed this limits. | ||
1077 | -- | ||
1078 | -- See also 'maxBitfieldSize', 'maxBlockSize' limits. | ||
1079 | -- | ||
1080 | maxMessageSize :: Int | ||
1081 | maxMessageSize = 20 + 1024 * 1024 | ||
1082 | |||
1083 | -- | This also limit max torrent size to: | ||
1084 | -- | ||
1085 | -- max_bitfield_size * piece_ix_per_byte * max_piece_size = | ||
1086 | -- 2 ^ 20 * 8 * 1MB = | ||
1087 | -- 8TB | ||
1088 | -- | ||
1089 | maxBitfieldSize :: Int | ||
1090 | maxBitfieldSize = 1024 * 1024 | ||
1091 | |||
1092 | getBitfield :: Int -> S.Get Bitfield | ||
1093 | getBitfield len | ||
1094 | | len > maxBitfieldSize = fail "BITFIELD message size exceeded limit" | ||
1095 | | otherwise = fromBitmap <$> getByteString len | ||
1096 | |||
1097 | maxBlockSize :: Int | ||
1098 | maxBlockSize = 4 * defaultTransferSize | ||
1099 | |||
1100 | getBlock :: Int -> S.Get (Block BL.ByteString) | ||
1101 | getBlock len | ||
1102 | | len > maxBlockSize = fail "BLOCK message size exceeded limit" | ||
1103 | | otherwise = Block <$> getInt <*> getInt | ||
1104 | <*> getLazyByteString (fromIntegral len) | ||
1105 | {-# INLINE getBlock #-} | ||
1106 | |||
1107 | instance Serialize Message where | ||
1108 | get = do | ||
1109 | len <- getInt | ||
1110 | |||
1111 | when (len > maxMessageSize) $ do | ||
1112 | fail "message body size exceeded the limit" | ||
1113 | |||
1114 | if len == 0 then return KeepAlive | ||
1115 | else do | ||
1116 | mid <- S.getWord8 | ||
1117 | case mid of | ||
1118 | 0x00 -> return $ Status (Choking True) | ||
1119 | 0x01 -> return $ Status (Choking False) | ||
1120 | 0x02 -> return $ Status (Interested True) | ||
1121 | 0x03 -> return $ Status (Interested False) | ||
1122 | 0x04 -> (Available . Have) <$> getInt | ||
1123 | 0x05 -> (Available . Bitfield) <$> getBitfield (pred len) | ||
1124 | 0x06 -> (Transfer . Request) <$> S.get | ||
1125 | 0x07 -> (Transfer . Piece) <$> getBlock (len - 9) | ||
1126 | 0x08 -> (Transfer . Cancel) <$> S.get | ||
1127 | 0x09 -> Port <$> S.get | ||
1128 | 0x0D -> (Fast . SuggestPiece) <$> getInt | ||
1129 | 0x0E -> return $ Fast HaveAll | ||
1130 | 0x0F -> return $ Fast HaveNone | ||
1131 | 0x10 -> (Fast . RejectRequest) <$> S.get | ||
1132 | 0x11 -> (Fast . AllowedFast) <$> getInt | ||
1133 | 0x14 -> Extended <$> getExtendedMessage (pred len) | ||
1134 | _ -> do | ||
1135 | rm <- S.remaining >>= S.getBytes | ||
1136 | fail $ "unknown message ID: " ++ show mid ++ "\n" | ||
1137 | ++ "remaining available bytes: " ++ show rm | ||
1138 | |||
1139 | put KeepAlive = putInt 0 | ||
1140 | put (Status msg) = putStatus msg | ||
1141 | put (Available msg) = putAvailable msg | ||
1142 | put (Transfer msg) = putTransfer msg | ||
1143 | put (Port p ) = putPort p | ||
1144 | put (Fast msg) = putFast msg | ||
1145 | put (Extended m ) = putExtendedMessage m | ||
1146 | |||
1147 | statusUpdateId :: StatusUpdate -> MessageId | ||
1148 | statusUpdateId (Choking choking) = fromIntegral (0 + fromEnum choking) | ||
1149 | statusUpdateId (Interested choking) = fromIntegral (2 + fromEnum choking) | ||
1150 | |||
1151 | putStatus :: Putter StatusUpdate | ||
1152 | putStatus su = do | ||
1153 | putInt 1 | ||
1154 | putWord8 (statusUpdateId su) | ||
1155 | |||
1156 | putAvailable :: Putter Available | ||
1157 | putAvailable (Have i) = do | ||
1158 | putInt 5 | ||
1159 | putWord8 0x04 | ||
1160 | putInt i | ||
1161 | putAvailable (Bitfield (toBitmap -> bs)) = do | ||
1162 | putInt $ 1 + fromIntegral (BL.length bs) | ||
1163 | putWord8 0x05 | ||
1164 | putLazyByteString bs | ||
1165 | |||
1166 | putBlock :: Putter (Block BL.ByteString) | ||
1167 | putBlock Block {..} = do | ||
1168 | putInt blkPiece | ||
1169 | putInt blkOffset | ||
1170 | putLazyByteString blkData | ||
1171 | |||
1172 | putTransfer :: Putter Transfer | ||
1173 | putTransfer (Request blk) = putInt 13 >> S.putWord8 0x06 >> S.put blk | ||
1174 | putTransfer (Piece blk) = do | ||
1175 | putInt (9 + blockSize blk) | ||
1176 | putWord8 0x07 | ||
1177 | putBlock blk | ||
1178 | putTransfer (Cancel blk) = putInt 13 >> S.putWord8 0x08 >> S.put blk | ||
1179 | |||
1180 | putPort :: Putter PortNumber | ||
1181 | putPort p = do | ||
1182 | putInt 3 | ||
1183 | putWord8 0x09 | ||
1184 | put p | ||
1185 | |||
1186 | putFast :: Putter FastMessage | ||
1187 | putFast HaveAll = putInt 1 >> putWord8 0x0E | ||
1188 | putFast HaveNone = putInt 1 >> putWord8 0x0F | ||
1189 | putFast (SuggestPiece pix) = putInt 5 >> putWord8 0x0D >> putInt pix | ||
1190 | putFast (RejectRequest i ) = putInt 13 >> putWord8 0x10 >> put i | ||
1191 | putFast (AllowedFast i ) = putInt 5 >> putWord8 0x11 >> putInt i | ||
1192 | |||
1193 | maxEHandshakeSize :: Int | ||
1194 | maxEHandshakeSize = 16 * 1024 | ||
1195 | |||
1196 | getExtendedHandshake :: Int -> S.Get ExtendedHandshake | ||
1197 | getExtendedHandshake messageSize | ||
1198 | | messageSize > maxEHandshakeSize | ||
1199 | = fail "extended handshake size exceeded limit" | ||
1200 | | otherwise = do | ||
1201 | bs <- getByteString messageSize | ||
1202 | either fail pure $ BE.decode bs | ||
1203 | |||
1204 | maxEUnknownSize :: Int | ||
1205 | maxEUnknownSize = 64 * 1024 | ||
1206 | |||
1207 | getExtendedUnknown :: Int -> S.Get BS.ByteString | ||
1208 | getExtendedUnknown len | ||
1209 | | len > maxEUnknownSize = fail "unknown extended message size exceeded limit" | ||
1210 | | otherwise = getByteString len | ||
1211 | |||
1212 | getExtendedMessage :: Int -> S.Get ExtendedMessage | ||
1213 | getExtendedMessage messageSize = do | ||
1214 | msgId <- getWord8 | ||
1215 | let msgBodySize = messageSize - 1 | ||
1216 | case msgId of | ||
1217 | 0 -> EHandshake <$> getExtendedHandshake msgBodySize | ||
1218 | 1 -> EMetadata msgId <$> getMetadata msgBodySize | ||
1219 | _ -> EUnknown msgId <$> getExtendedUnknown msgBodySize | ||
1220 | |||
1221 | -- | By spec. | ||
1222 | extendedMessageId :: MessageId | ||
1223 | extendedMessageId = 20 | ||
1224 | |||
1225 | putExt :: ExtendedMessageId -> BL.ByteString -> Put | ||
1226 | putExt mid lbs = do | ||
1227 | putWord32be $ fromIntegral (1 + 1 + BL.length lbs) | ||
1228 | putWord8 extendedMessageId | ||
1229 | putWord8 mid | ||
1230 | putLazyByteString lbs | ||
1231 | |||
1232 | -- NOTE: in contrast to getExtendedMessage this function put length | ||
1233 | -- and message id too! | ||
1234 | putExtendedMessage :: Putter ExtendedMessage | ||
1235 | putExtendedMessage (EHandshake hs) = putExt extHandshakeId $ BE.encode hs | ||
1236 | putExtendedMessage (EMetadata mid msg) = putExt mid $ putMetadata msg | ||
1237 | putExtendedMessage (EUnknown mid bs) = putExt mid $ BL.fromStrict bs | ||
diff --git a/dht/bittorrent/src/Network/BitTorrent/Exchange/Session.hs b/dht/bittorrent/src/Network/BitTorrent/Exchange/Session.hs new file mode 100644 index 00000000..38a3c3a6 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Internal/Cache.hs b/dht/bittorrent/src/Network/BitTorrent/Internal/Cache.hs new file mode 100644 index 00000000..8c74467a --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Internal/Progress.hs b/dht/bittorrent/src/Network/BitTorrent/Internal/Progress.hs new file mode 100644 index 00000000..6ac889e2 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Internal/Types.hs b/dht/bittorrent/src/Network/BitTorrent/Internal/Types.hs new file mode 100644 index 00000000..d157db3e --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Readme.md b/dht/bittorrent/src/Network/BitTorrent/Readme.md new file mode 100644 index 00000000..ebf9545e --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Tracker.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker.hs new file mode 100644 index 00000000..1191f921 --- /dev/null +++ b/dht/bittorrent/src/Network/BitTorrent/Tracker.hs | |||
@@ -0,0 +1,51 @@ | |||
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 | , trackers | ||
27 | , newSession | ||
28 | , closeSession | ||
29 | , withSession | ||
30 | |||
31 | -- ** Events | ||
32 | , AnnounceEvent (..) | ||
33 | , notify | ||
34 | , askPeers | ||
35 | |||
36 | -- ** Session state | ||
37 | , TrackerSession | ||
38 | , trackerPeers | ||
39 | , trackerScrape | ||
40 | |||
41 | , tryTakeData | ||
42 | , unsafeTryTakeData | ||
43 | |||
44 | , getSessionState | ||
45 | ) where | ||
46 | |||
47 | import Network.BitTorrent.Internal.Cache (tryTakeData, unsafeTryTakeData) | ||
48 | import Network.BitTorrent.Tracker.Message | ||
49 | import Network.BitTorrent.Tracker.List | ||
50 | import Network.BitTorrent.Tracker.RPC | ||
51 | import Network.BitTorrent.Tracker.Session | ||
diff --git a/dht/bittorrent/src/Network/BitTorrent/Tracker/List.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker/List.hs new file mode 100644 index 00000000..1507b4be --- /dev/null +++ b/dht/bittorrent/src/Network/BitTorrent/Tracker/List.hs | |||
@@ -0,0 +1,197 @@ | |||
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 | , trackers | ||
20 | , trackerList | ||
21 | , shuffleTiers | ||
22 | , mapWithURI | ||
23 | , Network.BitTorrent.Tracker.List.toList | ||
24 | |||
25 | -- * Traversals | ||
26 | , traverseAll | ||
27 | , traverseTiers | ||
28 | ) where | ||
29 | |||
30 | import Prelude hiding (mapM, foldr) | ||
31 | import Control.Arrow | ||
32 | import Control.Applicative | ||
33 | import Control.Exception | ||
34 | import Data.Default | ||
35 | import Data.List as L (map, elem, any, filter, null) | ||
36 | import Data.Maybe | ||
37 | import Data.Foldable | ||
38 | import Data.Traversable | ||
39 | import Network.URI | ||
40 | import System.Random.Shuffle | ||
41 | |||
42 | import Data.Torrent | ||
43 | import Network.BitTorrent.Tracker.RPC as RPC | ||
44 | |||
45 | {----------------------------------------------------------------------- | ||
46 | -- Tracker list datatype | ||
47 | -----------------------------------------------------------------------} | ||
48 | |||
49 | type TierEntry a = (URI, a) | ||
50 | type Tier a = [TierEntry a] | ||
51 | |||
52 | -- | Tracker list is either a single tracker or list of tiers. All | ||
53 | -- trackers in each tier must be checked before the client goes on to | ||
54 | -- the next tier. | ||
55 | data TrackerList a | ||
56 | = Announce (TierEntry a) -- ^ torrent file 'announce' field only | ||
57 | | TierList [Tier a] -- ^ torrent file 'announce-list' field only | ||
58 | deriving (Show, Eq) | ||
59 | |||
60 | -- | Empty tracker list. Can be used for trackerless torrents. | ||
61 | instance Default (TrackerList a) where | ||
62 | def = TierList [] | ||
63 | |||
64 | instance Functor TrackerList where | ||
65 | fmap f (Announce (uri, a)) = Announce (uri, f a) | ||
66 | fmap f (TierList a) = TierList (fmap (fmap (second f)) a) | ||
67 | |||
68 | instance Foldable TrackerList where | ||
69 | foldr f z (Announce e ) = f (snd e) z | ||
70 | foldr f z (TierList xs) = foldr (flip (foldr (f . snd))) z xs | ||
71 | |||
72 | _traverseEntry f (uri, a) = (,) uri <$> f a | ||
73 | |||
74 | instance Traversable TrackerList where | ||
75 | traverse f (Announce e ) = Announce <$> _traverseEntry f e | ||
76 | traverse f (TierList xs) = | ||
77 | TierList <$> traverse (traverse (_traverseEntry f)) xs | ||
78 | |||
79 | traverseWithURI :: Applicative f | ||
80 | => (TierEntry a -> f b) -> TrackerList a -> f (TrackerList b) | ||
81 | traverseWithURI f (Announce (uri, a)) = (Announce . (,) uri) <$> f (uri, a) | ||
82 | traverseWithURI f (TierList xxs ) = | ||
83 | TierList <$> traverse (traverse (traverseEntry f)) xxs | ||
84 | where | ||
85 | traverseEntry f (uri, a) = (,) uri <$> f (uri, a) | ||
86 | |||
87 | {----------------------------------------------------------------------- | ||
88 | -- List extraction | ||
89 | -----------------------------------------------------------------------} | ||
90 | -- BEP12 do not expose any restrictions for the content of | ||
91 | -- 'announce-list' key - there are some /bad/ cases can happen with | ||
92 | -- poorly designed or even malicious torrent creation software. | ||
93 | -- | ||
94 | -- Bad case #1: announce-list is present, but empty. | ||
95 | -- | ||
96 | -- { tAnnounce = Just "http://a.com" | ||
97 | -- , tAnnounceList = Just [[]] | ||
98 | -- } | ||
99 | -- | ||
100 | -- Bad case #2: announce uri do not present in announce list. | ||
101 | -- | ||
102 | -- { tAnnounce = Just "http://a.com" | ||
103 | -- , tAnnounceList = Just [["udp://a.com"]] | ||
104 | -- } | ||
105 | -- | ||
106 | -- The addBackup function solves both problems by adding announce uri | ||
107 | -- as backup tier. | ||
108 | -- | ||
109 | addBackup :: [[URI]] -> URI -> [[URI]] | ||
110 | addBackup tiers bkp | ||
111 | | L.any (L.elem bkp) tiers = tiers | ||
112 | | otherwise = tiers ++ [[bkp]] | ||
113 | |||
114 | fixList :: Maybe [[URI]] -> Maybe URI -> Maybe [[URI]] | ||
115 | fixList mxss mx = do | ||
116 | xss <- mxss | ||
117 | let xss' = L.filter (not . L.null) xss | ||
118 | return $ maybe xss' (addBackup xss') mx | ||
119 | |||
120 | trackers :: [URI] -> TrackerList () | ||
121 | trackers uris = TierList $ map (\uri -> [(uri,())]) uris | ||
122 | |||
123 | -- | Extract set of trackers from torrent file. The 'tAnnounce' key is | ||
124 | -- only ignored if the 'tAnnounceList' key is present. | ||
125 | trackerList :: Torrent -> TrackerList () | ||
126 | trackerList Torrent {..} = fromMaybe (TierList []) $ do | ||
127 | (TierList . tierList) <$> (tAnnounceList `fixList` tAnnounce) | ||
128 | <|> (Announce . nullEntry) <$> tAnnounce | ||
129 | where | ||
130 | nullEntry uri = (uri, ()) | ||
131 | tierList = L.map (L.map nullEntry) | ||
132 | |||
133 | -- | Shuffle /order of trackers/ in each tier, preserving original | ||
134 | -- /order of tiers/. This can help to balance the load between the | ||
135 | -- trackers. | ||
136 | shuffleTiers :: TrackerList a -> IO (TrackerList a) | ||
137 | shuffleTiers (Announce a ) = return (Announce a) | ||
138 | shuffleTiers (TierList xs) = TierList <$> mapM shuffleM xs | ||
139 | |||
140 | mapWithURI :: (URI -> a -> b) -> TrackerList a -> TrackerList b | ||
141 | mapWithURI f (Announce (uri, a)) = Announce (uri, f uri a) | ||
142 | mapWithURI f (TierList xs ) = TierList (L.map (L.map mapEntry) xs) | ||
143 | where | ||
144 | mapEntry (uri, a) = (uri, f uri a) | ||
145 | |||
146 | toList :: TrackerList a -> [[TierEntry a]] | ||
147 | toList (Announce e) = [[e]] | ||
148 | toList (TierList xxs) = xxs | ||
149 | |||
150 | {----------------------------------------------------------------------- | ||
151 | -- Special traversals (suppressed RPC exceptions) | ||
152 | -----------------------------------------------------------------------} | ||
153 | |||
154 | catchRPC :: IO a -> IO a -> IO a | ||
155 | catchRPC a b = catch a (f b) | ||
156 | where | ||
157 | f :: a -> RpcException -> a | ||
158 | f = const | ||
159 | |||
160 | throwRPC :: String -> IO a | ||
161 | throwRPC = throwIO . GenericException | ||
162 | |||
163 | -- | Like 'traverse' but ignores 'RpcExceptions'. | ||
164 | traverseAll :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a) | ||
165 | traverseAll action = traverseWithURI (action $?) | ||
166 | where | ||
167 | f $? x = catchRPC (f x) (return (snd x)) | ||
168 | |||
169 | -- | Like 'traverse' but put working trackers to the head of tiers. | ||
170 | -- This can help to avoid exceessive requests to not available | ||
171 | -- trackers at each reannounce. If no one action succeed then original | ||
172 | -- list is returned. | ||
173 | traverseTiers :: (TierEntry a -> IO a) -> TrackerList a -> IO (TrackerList a) | ||
174 | traverseTiers action ts = catchRPC (goList ts) (return ts) | ||
175 | where | ||
176 | goList tl @ (Announce _ ) = traverseWithURI action tl | ||
177 | goList (TierList tiers) = TierList <$> goTiers (goTier []) tiers | ||
178 | |||
179 | goTiers _ [] = throwRPC "traverseTiers: no tiers" | ||
180 | goTiers f (x : xs) = catchRPC shortcut failback | ||
181 | where | ||
182 | shortcut = do | ||
183 | x' <- f x | ||
184 | return (x' : xs) | ||
185 | |||
186 | failback = do | ||
187 | xs' <- goTiers f xs | ||
188 | return (x : xs') | ||
189 | |||
190 | goTier _ [] = throwRPC "traverseTiers: no trackers in tier" | ||
191 | goTier failed ((uri, a) : as) = catchRPC shortcut failback | ||
192 | where | ||
193 | shortcut = do | ||
194 | a' <- action (uri, a) | ||
195 | return ((uri, a') : as ++ failed) -- failed trackers at the end | ||
196 | |||
197 | failback = goTier ((uri, a) : failed) as | ||
diff --git a/dht/bittorrent/src/Network/BitTorrent/Tracker/Message.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker/Message.hs new file mode 100644 index 00000000..ab492275 --- /dev/null +++ b/dht/bittorrent/src/Network/BitTorrent/Tracker/Message.hs | |||
@@ -0,0 +1,925 @@ | |||
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 | {-# LANGUAGE CPP #-} | ||
31 | {-# OPTIONS -fno-warn-orphans #-} | ||
32 | module Network.BitTorrent.Tracker.Message | ||
33 | ( -- * Announce | ||
34 | -- ** Query | ||
35 | AnnounceEvent (..) | ||
36 | , AnnounceQuery (..) | ||
37 | , renderAnnounceQuery | ||
38 | , ParamParseFailure | ||
39 | , parseAnnounceQuery | ||
40 | |||
41 | -- ** Info | ||
42 | , PeerList (..) | ||
43 | , getPeerList | ||
44 | , AnnounceInfo(..) | ||
45 | , defaultNumWant | ||
46 | , defaultMaxNumWant | ||
47 | , defaultReannounceInterval | ||
48 | |||
49 | -- * Scrape | ||
50 | -- ** Query | ||
51 | , ScrapeQuery | ||
52 | , renderScrapeQuery | ||
53 | , parseScrapeQuery | ||
54 | |||
55 | -- ** Info | ||
56 | , ScrapeEntry (..) | ||
57 | , ScrapeInfo | ||
58 | |||
59 | -- * HTTP specific | ||
60 | -- ** Routes | ||
61 | , PathPiece | ||
62 | , defaultAnnouncePath | ||
63 | , defaultScrapePath | ||
64 | |||
65 | -- ** Preferences | ||
66 | , AnnouncePrefs (..) | ||
67 | , renderAnnouncePrefs | ||
68 | , parseAnnouncePrefs | ||
69 | |||
70 | -- ** Request | ||
71 | , AnnounceRequest (..) | ||
72 | , parseAnnounceRequest | ||
73 | , renderAnnounceRequest | ||
74 | |||
75 | -- ** Response | ||
76 | , announceType | ||
77 | , scrapeType | ||
78 | , parseFailureStatus | ||
79 | |||
80 | -- ** Extra | ||
81 | , queryToSimpleQuery | ||
82 | |||
83 | -- * UDP specific | ||
84 | -- ** Connection | ||
85 | , ConnectionId | ||
86 | , initialConnectionId | ||
87 | |||
88 | -- ** Messages | ||
89 | , Request (..) | ||
90 | , Response (..) | ||
91 | , responseName | ||
92 | |||
93 | -- ** Transaction | ||
94 | , genTransactionId | ||
95 | , TransactionId | ||
96 | , Transaction (..) | ||
97 | ) | ||
98 | where | ||
99 | |||
100 | import Control.Applicative | ||
101 | import Control.Monad | ||
102 | import Data.BEncode as BE hiding (Result) | ||
103 | import Data.BEncode.BDict as BE | ||
104 | import Data.ByteString as BS | ||
105 | import Data.ByteString.Char8 as BC | ||
106 | import Data.Char as Char | ||
107 | import Data.Convertible | ||
108 | import Data.Default | ||
109 | import Data.Either | ||
110 | import Data.List as L | ||
111 | import Data.Maybe | ||
112 | import Data.Monoid | ||
113 | import Data.Serialize as S hiding (Result) | ||
114 | import Data.String | ||
115 | import Data.Text (Text) | ||
116 | import Data.Text.Encoding | ||
117 | import Data.Typeable | ||
118 | import Data.Word | ||
119 | #if MIN_VERSION_iproute(1,7,4) | ||
120 | import Data.IP hiding (fromSockAddr) | ||
121 | #else | ||
122 | import Data.IP | ||
123 | #endif | ||
124 | import Network | ||
125 | import Network.HTTP.Types.QueryLike | ||
126 | import Network.HTTP.Types.URI hiding (urlEncode) | ||
127 | import Network.HTTP.Types.Status | ||
128 | import Network.Socket hiding (Connected) | ||
129 | import Numeric | ||
130 | import System.Entropy | ||
131 | import Text.Read (readMaybe) | ||
132 | |||
133 | import Data.Torrent | ||
134 | import Network.Address | ||
135 | import Network.BitTorrent.Internal.Progress | ||
136 | |||
137 | {----------------------------------------------------------------------- | ||
138 | -- Events | ||
139 | -----------------------------------------------------------------------} | ||
140 | |||
141 | -- | Events are used to specify which kind of announce query is performed. | ||
142 | data AnnounceEvent | ||
143 | -- | For the first request: when download first begins. | ||
144 | = Started | ||
145 | |||
146 | -- | This peer stopped downloading /and/ uploading the torrent or | ||
147 | -- just shutting down. | ||
148 | | Stopped | ||
149 | |||
150 | -- | This peer completed downloading the torrent. This only happen | ||
151 | -- right after last piece have been verified. No 'Completed' is | ||
152 | -- sent if the file was completed when 'Started'. | ||
153 | | Completed | ||
154 | deriving (Show, Read, Eq, Ord, Enum, Bounded, Typeable) | ||
155 | |||
156 | -- | HTTP tracker protocol compatible encoding. | ||
157 | instance QueryValueLike AnnounceEvent where | ||
158 | toQueryValue e = toQueryValue (Char.toLower x : xs) | ||
159 | where | ||
160 | (x : xs) = show e -- INVARIANT: this is always nonempty list | ||
161 | |||
162 | type EventId = Word32 | ||
163 | |||
164 | -- | UDP tracker encoding event codes. | ||
165 | eventId :: AnnounceEvent -> EventId | ||
166 | eventId Completed = 1 | ||
167 | eventId Started = 2 | ||
168 | eventId Stopped = 3 | ||
169 | |||
170 | -- TODO add Regular event | ||
171 | putEvent :: Putter (Maybe AnnounceEvent) | ||
172 | putEvent Nothing = putWord32be 0 | ||
173 | putEvent (Just e) = putWord32be (eventId e) | ||
174 | |||
175 | getEvent :: S.Get (Maybe AnnounceEvent) | ||
176 | getEvent = do | ||
177 | eid <- getWord32be | ||
178 | case eid of | ||
179 | 0 -> return Nothing | ||
180 | 1 -> return $ Just Completed | ||
181 | 2 -> return $ Just Started | ||
182 | 3 -> return $ Just Stopped | ||
183 | _ -> fail "unknown event id" | ||
184 | |||
185 | {----------------------------------------------------------------------- | ||
186 | Announce query | ||
187 | -----------------------------------------------------------------------} | ||
188 | -- TODO add &ipv6= and &ipv4= params to AnnounceQuery | ||
189 | -- http://www.bittorrent.org/beps/bep_0007.html#announce-parameter | ||
190 | |||
191 | -- | A tracker request is HTTP GET request; used to include metrics | ||
192 | -- from clients that help the tracker keep overall statistics about | ||
193 | -- the torrent. The most important, requests are used by the tracker | ||
194 | -- to keep track lists of active peer for a particular torrent. | ||
195 | -- | ||
196 | data AnnounceQuery = AnnounceQuery | ||
197 | { | ||
198 | -- | Hash of info part of the torrent usually obtained from | ||
199 | -- 'Torrent' or 'Magnet'. | ||
200 | reqInfoHash :: !InfoHash | ||
201 | |||
202 | -- | ID of the peer doing request. | ||
203 | , reqPeerId :: !PeerId | ||
204 | |||
205 | -- | Port to listen to for connections from other | ||
206 | -- peers. Tracker should respond with this port when | ||
207 | -- some /other/ peer request the tracker with the same info hash. | ||
208 | -- Normally, this port is choosed from 'defaultPorts'. | ||
209 | , reqPort :: !PortNumber | ||
210 | |||
211 | -- | Current progress of peer doing request. | ||
212 | , reqProgress :: !Progress | ||
213 | |||
214 | -- | The peer IP. Needed only when client communicated with | ||
215 | -- tracker throught a proxy. | ||
216 | , reqIP :: Maybe HostAddress | ||
217 | |||
218 | -- | Number of peers that the peers wants to receive from. It is | ||
219 | -- optional for trackers to honor this limit. See note for | ||
220 | -- 'defaultNumWant'. | ||
221 | , reqNumWant :: Maybe Int | ||
222 | |||
223 | -- | If not specified, the request is regular periodic | ||
224 | -- request. Regular request should be sent | ||
225 | , reqEvent :: Maybe AnnounceEvent | ||
226 | } deriving (Show, Eq, Typeable) | ||
227 | |||
228 | -- | UDP tracker protocol compatible encoding. | ||
229 | instance Serialize AnnounceQuery where | ||
230 | put AnnounceQuery {..} = do | ||
231 | put reqInfoHash | ||
232 | put reqPeerId | ||
233 | put reqProgress | ||
234 | putEvent reqEvent | ||
235 | putWord32host $ fromMaybe 0 reqIP | ||
236 | putWord32be $ 0 -- TODO what the fuck is "key"? | ||
237 | putWord32be $ fromIntegral $ fromMaybe (-1) reqNumWant | ||
238 | |||
239 | put reqPort | ||
240 | |||
241 | get = do | ||
242 | ih <- get | ||
243 | pid <- get | ||
244 | |||
245 | progress <- get | ||
246 | |||
247 | ev <- getEvent | ||
248 | ip <- getWord32be | ||
249 | -- key <- getWord32be -- TODO | ||
250 | want <- getWord32be | ||
251 | |||
252 | port <- get | ||
253 | |||
254 | return $ AnnounceQuery { | ||
255 | reqInfoHash = ih | ||
256 | , reqPeerId = pid | ||
257 | , reqPort = port | ||
258 | , reqProgress = progress | ||
259 | , reqIP = if ip == 0 then Nothing else Just ip | ||
260 | , reqNumWant = if want == -1 then Nothing | ||
261 | else Just (fromIntegral want) | ||
262 | , reqEvent = ev | ||
263 | } | ||
264 | |||
265 | instance QueryValueLike PortNumber where | ||
266 | toQueryValue = toQueryValue . show . fromEnum | ||
267 | |||
268 | instance QueryValueLike Word32 where | ||
269 | toQueryValue = toQueryValue . show | ||
270 | |||
271 | instance QueryValueLike Int where | ||
272 | toQueryValue = toQueryValue . show | ||
273 | |||
274 | -- | HTTP tracker protocol compatible encoding. | ||
275 | instance QueryLike AnnounceQuery where | ||
276 | toQuery AnnounceQuery {..} = | ||
277 | toQuery reqProgress ++ | ||
278 | [ ("info_hash", toQueryValue reqInfoHash) -- TODO use 'paramName' | ||
279 | , ("peer_id" , toQueryValue reqPeerId) | ||
280 | , ("port" , toQueryValue reqPort) | ||
281 | , ("ip" , toQueryValue reqIP) | ||
282 | , ("numwant" , toQueryValue reqNumWant) | ||
283 | , ("event" , toQueryValue reqEvent) | ||
284 | ] | ||
285 | |||
286 | -- | Filter @param=value@ pairs with the unset value. | ||
287 | queryToSimpleQuery :: Query -> SimpleQuery | ||
288 | queryToSimpleQuery = catMaybes . L.map f | ||
289 | where | ||
290 | f (_, Nothing) = Nothing | ||
291 | f (a, Just b ) = Just (a, b) | ||
292 | |||
293 | -- | Encode announce query to query string. | ||
294 | renderAnnounceQuery :: AnnounceQuery -> SimpleQuery | ||
295 | renderAnnounceQuery = queryToSimpleQuery . toQuery | ||
296 | |||
297 | data QueryParam | ||
298 | -- announce query | ||
299 | = ParamInfoHash | ||
300 | | ParamPeerId | ||
301 | | ParamPort | ||
302 | | ParamUploaded | ||
303 | | ParamLeft | ||
304 | | ParamDownloaded | ||
305 | | ParamIP | ||
306 | | ParamNumWant | ||
307 | | ParamEvent | ||
308 | -- announce query ext | ||
309 | | ParamCompact | ||
310 | | ParamNoPeerId | ||
311 | deriving (Show, Eq, Ord, Enum) | ||
312 | |||
313 | paramName :: QueryParam -> BS.ByteString | ||
314 | paramName ParamInfoHash = "info_hash" | ||
315 | paramName ParamPeerId = "peer_id" | ||
316 | paramName ParamPort = "port" | ||
317 | paramName ParamUploaded = "uploaded" | ||
318 | paramName ParamLeft = "left" | ||
319 | paramName ParamDownloaded = "downloaded" | ||
320 | paramName ParamIP = "ip" | ||
321 | paramName ParamNumWant = "numwant" | ||
322 | paramName ParamEvent = "event" | ||
323 | paramName ParamCompact = "compact" | ||
324 | paramName ParamNoPeerId = "no_peer_id" | ||
325 | {-# INLINE paramName #-} | ||
326 | |||
327 | class FromParam a where | ||
328 | fromParam :: BS.ByteString -> Maybe a | ||
329 | |||
330 | instance FromParam Bool where | ||
331 | fromParam "0" = Just False | ||
332 | fromParam "1" = Just True | ||
333 | fromParam _ = Nothing | ||
334 | |||
335 | instance FromParam InfoHash where | ||
336 | fromParam = either (const Nothing) pure . safeConvert | ||
337 | |||
338 | instance FromParam PeerId where | ||
339 | fromParam = either (const Nothing) pure . safeConvert | ||
340 | |||
341 | instance FromParam Word32 where | ||
342 | fromParam = readMaybe . BC.unpack | ||
343 | |||
344 | instance FromParam Word64 where | ||
345 | fromParam = readMaybe . BC.unpack | ||
346 | |||
347 | instance FromParam Int where | ||
348 | fromParam = readMaybe . BC.unpack | ||
349 | |||
350 | instance FromParam PortNumber where | ||
351 | fromParam bs = fromIntegral <$> (fromParam bs :: Maybe Word32) | ||
352 | |||
353 | instance FromParam AnnounceEvent where | ||
354 | fromParam bs = do | ||
355 | (x, xs) <- BC.uncons bs | ||
356 | readMaybe $ BC.unpack $ BC.cons (Char.toUpper x) xs | ||
357 | |||
358 | -- | 'ParamParseFailure' represent errors can occur while parsing HTTP | ||
359 | -- tracker requests. In case of failure, this can be used to provide | ||
360 | -- more informative 'statusCode' and 'statusMessage' in tracker | ||
361 | -- responses. | ||
362 | -- | ||
363 | data ParamParseFailure | ||
364 | = Missing QueryParam -- ^ param not found in query string; | ||
365 | | Invalid QueryParam BS.ByteString -- ^ param present but not valid. | ||
366 | deriving (Show, Eq) | ||
367 | |||
368 | type ParseResult = Either ParamParseFailure | ||
369 | |||
370 | withError :: ParamParseFailure -> Maybe a -> ParseResult a | ||
371 | withError e = maybe (Left e) Right | ||
372 | |||
373 | reqParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult a | ||
374 | reqParam param xs = do | ||
375 | val <- withError (Missing param) $ L.lookup (paramName param) xs | ||
376 | withError (Invalid param val) (fromParam val) | ||
377 | |||
378 | optParam :: FromParam a => QueryParam -> SimpleQuery -> ParseResult (Maybe a) | ||
379 | optParam param ps | ||
380 | | Just x <- L.lookup (paramName param) ps | ||
381 | = pure <$> withError (Invalid param x) (fromParam x) | ||
382 | | otherwise = pure Nothing | ||
383 | |||
384 | parseProgress :: SimpleQuery -> ParseResult Progress | ||
385 | parseProgress params = Progress | ||
386 | <$> reqParam ParamDownloaded params | ||
387 | <*> reqParam ParamLeft params | ||
388 | <*> reqParam ParamUploaded params | ||
389 | |||
390 | -- | Parse announce request from a query string. | ||
391 | parseAnnounceQuery :: SimpleQuery -> ParseResult AnnounceQuery | ||
392 | parseAnnounceQuery params = AnnounceQuery | ||
393 | <$> reqParam ParamInfoHash params | ||
394 | <*> reqParam ParamPeerId params | ||
395 | <*> reqParam ParamPort params | ||
396 | <*> parseProgress params | ||
397 | <*> optParam ParamIP params | ||
398 | <*> optParam ParamNumWant params | ||
399 | <*> optParam ParamEvent params | ||
400 | |||
401 | {----------------------------------------------------------------------- | ||
402 | -- Announce Info | ||
403 | -----------------------------------------------------------------------} | ||
404 | -- TODO check if announceinterval/complete/incomplete is positive ints | ||
405 | |||
406 | -- | Tracker can return peer list in either compact(BEP23) or not | ||
407 | -- compact form. | ||
408 | -- | ||
409 | -- For more info see: <http://www.bittorrent.org/beps/bep_0023.html> | ||
410 | -- | ||
411 | data PeerList ip | ||
412 | = PeerList [PeerAddr] | ||
413 | | CompactPeerList [PeerAddr] | ||
414 | deriving (Show, Eq, Typeable, Functor) | ||
415 | |||
416 | -- | The empty non-compact peer list. | ||
417 | instance Default (PeerList IP) where | ||
418 | def = PeerList [] | ||
419 | {-# INLINE def #-} | ||
420 | |||
421 | getPeerList :: PeerList IP -> [PeerAddr] | ||
422 | getPeerList (PeerList xs) = xs | ||
423 | getPeerList (CompactPeerList xs) = xs | ||
424 | |||
425 | instance BEncode (PeerList a) where | ||
426 | toBEncode (PeerList xs) = toBEncode xs | ||
427 | toBEncode (CompactPeerList xs) = toBEncode $ runPut (mapM_ put xs) | ||
428 | |||
429 | fromBEncode (BList l ) = PeerList <$> fromBEncode (BList l) | ||
430 | fromBEncode (BString s ) = CompactPeerList <$> runGet (many get) s | ||
431 | fromBEncode _ = decodingError "PeerList: should be a BString or BList" | ||
432 | |||
433 | -- | The tracker response includes a peer list that helps the client | ||
434 | -- participate in the torrent. The most important is 'respPeer' list | ||
435 | -- used to join the swarm. | ||
436 | -- | ||
437 | data AnnounceInfo = | ||
438 | Failure !Text -- ^ Failure reason in human readable form. | ||
439 | | AnnounceInfo { | ||
440 | -- | Number of peers completed the torrent. (seeders) | ||
441 | respComplete :: !(Maybe Int) | ||
442 | |||
443 | -- | Number of peers downloading the torrent. (leechers) | ||
444 | , respIncomplete :: !(Maybe Int) | ||
445 | |||
446 | -- | Recommended interval to wait between requests, in seconds. | ||
447 | , respInterval :: !Int | ||
448 | |||
449 | -- | Minimal amount of time between requests, in seconds. A | ||
450 | -- peer /should/ make timeout with at least 'respMinInterval' | ||
451 | -- value, otherwise tracker might not respond. If not specified | ||
452 | -- the same applies to 'respInterval'. | ||
453 | , respMinInterval :: !(Maybe Int) | ||
454 | |||
455 | -- | Peers that must be contacted. | ||
456 | , respPeers :: !(PeerList IP) | ||
457 | |||
458 | -- | Human readable warning. | ||
459 | , respWarning :: !(Maybe Text) | ||
460 | } deriving (Show, Eq, Typeable) | ||
461 | |||
462 | -- | Empty peer list with default reannounce interval. | ||
463 | instance Default AnnounceInfo where | ||
464 | def = AnnounceInfo | ||
465 | { respComplete = Nothing | ||
466 | , respIncomplete = Nothing | ||
467 | , respInterval = defaultReannounceInterval | ||
468 | , respMinInterval = Nothing | ||
469 | , respPeers = def | ||
470 | , respWarning = Nothing | ||
471 | } | ||
472 | |||
473 | -- | HTTP tracker protocol compatible encoding. | ||
474 | instance BEncode AnnounceInfo where | ||
475 | toBEncode (Failure t) = toDict $ | ||
476 | "failure reason" .=! t | ||
477 | .: endDict | ||
478 | |||
479 | toBEncode AnnounceInfo {..} = toDict $ | ||
480 | "complete" .=? respComplete | ||
481 | .: "incomplete" .=? respIncomplete | ||
482 | .: "interval" .=! respInterval | ||
483 | .: "min interval" .=? respMinInterval | ||
484 | .: "peers" .=! peers | ||
485 | .: "peers6" .=? peers6 | ||
486 | .: "warning message" .=? respWarning | ||
487 | .: endDict | ||
488 | where | ||
489 | (peers, peers6) = prttn respPeers | ||
490 | |||
491 | prttn :: PeerList IP -> (PeerList IPv4, Maybe (PeerList IPv6)) | ||
492 | prttn (PeerList xs) = (PeerList xs, Nothing) | ||
493 | prttn (CompactPeerList xs) = mk $ partitionEithers $ toEither <$> xs | ||
494 | where | ||
495 | mk (v4s, v6s) | ||
496 | | L.null v6s = (CompactPeerList v4s, Nothing) | ||
497 | | otherwise = (CompactPeerList v4s, Just (CompactPeerList v6s)) | ||
498 | |||
499 | toEither :: PeerAddr -> Either PeerAddr PeerAddr | ||
500 | toEither PeerAddr {..} = case peerHost of | ||
501 | ipv4@IPv4{} -> Left $ PeerAddr peerId ipv4 peerPort | ||
502 | ipv6@IPv6{} -> Right $ PeerAddr peerId ipv6 peerPort | ||
503 | |||
504 | fromBEncode (BDict d) | ||
505 | | Just t <- BE.lookup "failure reason" d = Failure <$> fromBEncode t | ||
506 | | otherwise = (`fromDict` (BDict d)) $ | ||
507 | AnnounceInfo | ||
508 | <$>? "complete" | ||
509 | <*>? "incomplete" | ||
510 | <*>! "interval" | ||
511 | <*>? "min interval" | ||
512 | <*> (uncurry merge =<< (,) <$>! "peers" <*>? "peers6") | ||
513 | <*>? "warning message" | ||
514 | where | ||
515 | merge :: PeerList IPv4 -> Maybe (PeerList IPv6) -> BE.Get (PeerList IP) | ||
516 | merge (PeerList ips) Nothing = pure (PeerList ips) | ||
517 | merge (PeerList _ ) (Just _) | ||
518 | = fail "PeerList: non-compact peer list provided, \ | ||
519 | \but the `peers6' field present" | ||
520 | |||
521 | merge (CompactPeerList ipv4s) Nothing | ||
522 | = pure $ CompactPeerList ipv4s | ||
523 | |||
524 | merge (CompactPeerList _ ) (Just (PeerList _)) | ||
525 | = fail "PeerList: the `peers6' field value \ | ||
526 | \should contain *compact* peer list" | ||
527 | |||
528 | merge (CompactPeerList ipv4s) (Just (CompactPeerList ipv6s)) | ||
529 | = pure $ CompactPeerList $ | ||
530 | ipv4s <> ipv6s | ||
531 | |||
532 | fromBEncode _ = decodingError "Announce info" | ||
533 | |||
534 | -- | UDP tracker protocol compatible encoding. | ||
535 | instance Serialize AnnounceInfo where | ||
536 | put (Failure msg) = put $ encodeUtf8 msg | ||
537 | put AnnounceInfo {..} = do | ||
538 | putWord32be $ fromIntegral respInterval | ||
539 | putWord32be $ fromIntegral $ fromMaybe 0 respIncomplete | ||
540 | putWord32be $ fromIntegral $ fromMaybe 0 respComplete | ||
541 | forM_ (getPeerList respPeers) put | ||
542 | |||
543 | get = do | ||
544 | interval <- getWord32be | ||
545 | leechers <- getWord32be | ||
546 | seeders <- getWord32be | ||
547 | peers <- many $ isolate 6 get -- isolated to specify IPv4. | ||
548 | |||
549 | return $ AnnounceInfo { | ||
550 | respWarning = Nothing | ||
551 | , respInterval = fromIntegral interval | ||
552 | , respMinInterval = Nothing | ||
553 | , respIncomplete = Just $ fromIntegral leechers | ||
554 | , respComplete = Just $ fromIntegral seeders | ||
555 | , respPeers = PeerList peers | ||
556 | } | ||
557 | |||
558 | -- | Decodes announce response from bencoded string, for debugging only. | ||
559 | instance IsString AnnounceInfo where | ||
560 | fromString str = either (error . format) id $ BE.decode (fromString str) | ||
561 | where | ||
562 | format msg = "fromString: unable to decode AnnounceInfo: " ++ msg | ||
563 | |||
564 | -- | Above 25, new peers are highly unlikely to increase download | ||
565 | -- speed. Even 30 peers is /plenty/, the official client version 3 | ||
566 | -- in fact only actively forms new connections if it has less than | ||
567 | -- 30 peers and will refuse connections if it has 55. | ||
568 | -- | ||
569 | -- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Basic_Tracker_Announce_Request> | ||
570 | -- | ||
571 | defaultNumWant :: Int | ||
572 | defaultNumWant = 50 | ||
573 | |||
574 | -- | Reasonable upper bound of numwant parameter. | ||
575 | defaultMaxNumWant :: Int | ||
576 | defaultMaxNumWant = 200 | ||
577 | |||
578 | -- | Widely used reannounce interval. Note: tracker clients should not | ||
579 | -- use this value! | ||
580 | defaultReannounceInterval :: Int | ||
581 | defaultReannounceInterval = 30 * 60 | ||
582 | |||
583 | {----------------------------------------------------------------------- | ||
584 | Scrape message | ||
585 | -----------------------------------------------------------------------} | ||
586 | |||
587 | -- | Scrape query used to specify a set of torrent to scrape. | ||
588 | -- If list is empty then tracker should return scrape info about each | ||
589 | -- torrent. | ||
590 | type ScrapeQuery = [InfoHash] | ||
591 | |||
592 | -- TODO | ||
593 | -- data ScrapeQuery | ||
594 | -- = ScrapeAll | ||
595 | -- | ScrapeSingle InfoHash | ||
596 | -- | ScrapeMulti (HashSet InfoHash) | ||
597 | -- deriving (Show) | ||
598 | -- | ||
599 | -- data ScrapeInfo | ||
600 | -- = ScrapeAll (HashMap InfoHash ScrapeEntry) | ||
601 | -- | ScrapeSingle InfoHash ScrapeEntry | ||
602 | -- | ScrapeMulti (HashMap InfoHash ScrapeEntry) | ||
603 | -- | ||
604 | |||
605 | scrapeParam :: BS.ByteString | ||
606 | scrapeParam = "info_hash" | ||
607 | |||
608 | isScrapeParam :: BS.ByteString -> Bool | ||
609 | isScrapeParam = (==) scrapeParam | ||
610 | |||
611 | -- | Parse scrape query to query string. | ||
612 | parseScrapeQuery :: SimpleQuery -> ScrapeQuery | ||
613 | parseScrapeQuery | ||
614 | = catMaybes . L.map (fromParam . snd) . L.filter (isScrapeParam . fst) | ||
615 | |||
616 | -- | Render scrape query to query string. | ||
617 | renderScrapeQuery :: ScrapeQuery -> SimpleQuery | ||
618 | renderScrapeQuery = queryToSimpleQuery . L.map mkPair | ||
619 | where | ||
620 | mkPair ih = (scrapeParam, toQueryValue ih) | ||
621 | |||
622 | -- | Overall information about particular torrent. | ||
623 | data ScrapeEntry = ScrapeEntry { | ||
624 | -- | Number of seeders - peers with the entire file. | ||
625 | siComplete :: {-# UNPACK #-} !Int | ||
626 | |||
627 | -- | Total number of times the tracker has registered a completion. | ||
628 | , siDownloaded :: {-# UNPACK #-} !Int | ||
629 | |||
630 | -- | Number of leechers. | ||
631 | , siIncomplete :: {-# UNPACK #-} !Int | ||
632 | |||
633 | -- | Name of the torrent file, as specified by the "name" | ||
634 | -- file in the info section of the .torrent file. | ||
635 | , siName :: !(Maybe Text) | ||
636 | } deriving (Show, Eq, Typeable) | ||
637 | |||
638 | -- | HTTP tracker protocol compatible encoding. | ||
639 | instance BEncode ScrapeEntry where | ||
640 | toBEncode ScrapeEntry {..} = toDict $ | ||
641 | "complete" .=! siComplete | ||
642 | .: "downloaded" .=! siDownloaded | ||
643 | .: "incomplete" .=! siIncomplete | ||
644 | .: "name" .=? siName | ||
645 | .: endDict | ||
646 | |||
647 | fromBEncode = fromDict $ ScrapeEntry | ||
648 | <$>! "complete" | ||
649 | <*>! "downloaded" | ||
650 | <*>! "incomplete" | ||
651 | <*>? "name" | ||
652 | |||
653 | -- | UDP tracker protocol compatible encoding. | ||
654 | instance Serialize ScrapeEntry where | ||
655 | put ScrapeEntry {..} = do | ||
656 | putWord32be $ fromIntegral siComplete | ||
657 | putWord32be $ fromIntegral siDownloaded | ||
658 | putWord32be $ fromIntegral siIncomplete | ||
659 | |||
660 | get = ScrapeEntry | ||
661 | <$> (fromIntegral <$> getWord32be) | ||
662 | <*> (fromIntegral <$> getWord32be) | ||
663 | <*> (fromIntegral <$> getWord32be) | ||
664 | <*> pure Nothing | ||
665 | |||
666 | -- | Scrape info about a set of torrents. | ||
667 | type ScrapeInfo = [(InfoHash, ScrapeEntry)] | ||
668 | |||
669 | {----------------------------------------------------------------------- | ||
670 | -- HTTP specific | ||
671 | -----------------------------------------------------------------------} | ||
672 | |||
673 | -- | Some HTTP trackers allow to choose prefered representation of the | ||
674 | -- 'AnnounceInfo'. It's optional for trackers to honor any of this | ||
675 | -- options. | ||
676 | data AnnouncePrefs = AnnouncePrefs | ||
677 | { -- | If specified, "compact" parameter is used to advise the | ||
678 | -- tracker to send peer id list as: | ||
679 | -- | ||
680 | -- * bencoded list (extCompact = Just False); | ||
681 | -- * or more compact binary string (extCompact = Just True). | ||
682 | -- | ||
683 | -- The later is prefered since compact peer list will reduce the | ||
684 | -- size of tracker responses. Hovewer, if tracker do not support | ||
685 | -- this extension then it can return peer list in either form. | ||
686 | -- | ||
687 | -- For more info see: <http://www.bittorrent.org/beps/bep_0023.html> | ||
688 | -- | ||
689 | extCompact :: !(Maybe Bool) | ||
690 | |||
691 | -- | If specified, "no_peer_id" parameter is used advise tracker | ||
692 | -- to either send or not to send peer id in tracker response. | ||
693 | -- Tracker may not support this extension as well. | ||
694 | -- | ||
695 | -- For more info see: | ||
696 | -- <http://permalink.gmane.org/gmane.network.bit-torrent.general/4030> | ||
697 | -- | ||
698 | , extNoPeerId :: !(Maybe Bool) | ||
699 | } deriving (Show, Eq, Typeable) | ||
700 | |||
701 | instance Default AnnouncePrefs where | ||
702 | def = AnnouncePrefs Nothing Nothing | ||
703 | |||
704 | instance QueryLike AnnouncePrefs where | ||
705 | toQuery AnnouncePrefs {..} = | ||
706 | [ ("compact", toQueryFlag <$> extCompact) -- TODO use 'paramName' | ||
707 | , ("no_peer_id", toQueryFlag <$> extNoPeerId) | ||
708 | ] | ||
709 | where | ||
710 | toQueryFlag False = "0" | ||
711 | toQueryFlag True = "1" | ||
712 | |||
713 | -- | Parse announce query extended part from query string. | ||
714 | parseAnnouncePrefs :: SimpleQuery -> AnnouncePrefs | ||
715 | parseAnnouncePrefs params = either (const def) id $ | ||
716 | AnnouncePrefs | ||
717 | <$> optParam ParamCompact params | ||
718 | <*> optParam ParamNoPeerId params | ||
719 | |||
720 | -- | Render announce preferences to query string. | ||
721 | renderAnnouncePrefs :: AnnouncePrefs -> SimpleQuery | ||
722 | renderAnnouncePrefs = queryToSimpleQuery . toQuery | ||
723 | |||
724 | -- | HTTP tracker request with preferences. | ||
725 | data AnnounceRequest = AnnounceRequest | ||
726 | { announceQuery :: AnnounceQuery -- ^ Request query params. | ||
727 | , announcePrefs :: AnnouncePrefs -- ^ Optional advises to the tracker. | ||
728 | } deriving (Show, Eq, Typeable) | ||
729 | |||
730 | instance QueryLike AnnounceRequest where | ||
731 | toQuery AnnounceRequest{..} = | ||
732 | toQuery announcePrefs <> | ||
733 | toQuery announceQuery | ||
734 | |||
735 | -- | Parse announce request from query string. | ||
736 | parseAnnounceRequest :: SimpleQuery -> ParseResult AnnounceRequest | ||
737 | parseAnnounceRequest params = AnnounceRequest | ||
738 | <$> parseAnnounceQuery params | ||
739 | <*> pure (parseAnnouncePrefs params) | ||
740 | |||
741 | -- | Render announce request to query string. | ||
742 | renderAnnounceRequest :: AnnounceRequest -> SimpleQuery | ||
743 | renderAnnounceRequest = queryToSimpleQuery . toQuery | ||
744 | |||
745 | type PathPiece = BS.ByteString | ||
746 | |||
747 | defaultAnnouncePath :: PathPiece | ||
748 | defaultAnnouncePath = "announce" | ||
749 | |||
750 | defaultScrapePath :: PathPiece | ||
751 | defaultScrapePath = "scrape" | ||
752 | |||
753 | missingOffset :: Int | ||
754 | missingOffset = 101 | ||
755 | |||
756 | invalidOffset :: Int | ||
757 | invalidOffset = 150 | ||
758 | |||
759 | parseFailureCode :: ParamParseFailure -> Int | ||
760 | parseFailureCode (Missing param ) = missingOffset + fromEnum param | ||
761 | parseFailureCode (Invalid param _) = invalidOffset + fromEnum param | ||
762 | |||
763 | parseFailureMessage :: ParamParseFailure -> BS.ByteString | ||
764 | parseFailureMessage e = BS.concat $ case e of | ||
765 | Missing p -> ["Missing parameter: ", paramName p] | ||
766 | Invalid p v -> ["Invalid parameter: ", paramName p, " = ", v] | ||
767 | |||
768 | -- | HTTP response /content type/ for announce info. | ||
769 | announceType :: ByteString | ||
770 | announceType = "text/plain" | ||
771 | |||
772 | -- | HTTP response /content type/ for scrape info. | ||
773 | scrapeType :: ByteString | ||
774 | scrapeType = "text/plain" | ||
775 | |||
776 | -- | Get HTTP response status from a announce params parse failure. | ||
777 | -- | ||
778 | -- For more info see: | ||
779 | -- <https://wiki.theory.org/BitTorrent_Tracker_Protocol#Response_Codes> | ||
780 | -- | ||
781 | parseFailureStatus :: ParamParseFailure -> Status | ||
782 | parseFailureStatus = mkStatus <$> parseFailureCode <*> parseFailureMessage | ||
783 | |||
784 | {----------------------------------------------------------------------- | ||
785 | -- UDP specific message types | ||
786 | -----------------------------------------------------------------------} | ||
787 | |||
788 | genToken :: IO Word64 | ||
789 | genToken = do | ||
790 | bs <- getEntropy 8 | ||
791 | either err return $ runGet getWord64be bs | ||
792 | where | ||
793 | err = error "genToken: impossible happen" | ||
794 | |||
795 | -- | Connection Id is used for entire tracker session. | ||
796 | newtype ConnectionId = ConnectionId Word64 | ||
797 | deriving (Eq, Serialize) | ||
798 | |||
799 | instance Show ConnectionId where | ||
800 | showsPrec _ (ConnectionId cid) = showString "0x" <> showHex cid | ||
801 | |||
802 | initialConnectionId :: ConnectionId | ||
803 | initialConnectionId = ConnectionId 0x41727101980 | ||
804 | |||
805 | -- | Transaction Id is used within a UDP RPC. | ||
806 | newtype TransactionId = TransactionId Word32 | ||
807 | deriving (Eq, Ord, Enum, Bounded, Serialize) | ||
808 | |||
809 | instance Show TransactionId where | ||
810 | showsPrec _ (TransactionId tid) = showString "0x" <> showHex tid | ||
811 | |||
812 | genTransactionId :: IO TransactionId | ||
813 | genTransactionId = (TransactionId . fromIntegral) <$> genToken | ||
814 | |||
815 | data Request | ||
816 | = Connect | ||
817 | | Announce AnnounceQuery | ||
818 | | Scrape ScrapeQuery | ||
819 | deriving Show | ||
820 | |||
821 | data Response | ||
822 | = Connected ConnectionId | ||
823 | | Announced AnnounceInfo | ||
824 | | Scraped [ScrapeEntry] | ||
825 | | Failed Text | ||
826 | deriving Show | ||
827 | |||
828 | responseName :: Response -> String | ||
829 | responseName (Connected _) = "connected" | ||
830 | responseName (Announced _) = "announced" | ||
831 | responseName (Scraped _) = "scraped" | ||
832 | responseName (Failed _) = "failed" | ||
833 | |||
834 | data family Transaction a | ||
835 | data instance Transaction Request = TransactionQ | ||
836 | { connIdQ :: {-# UNPACK #-} !ConnectionId | ||
837 | , transIdQ :: {-# UNPACK #-} !TransactionId | ||
838 | , request :: !Request | ||
839 | } deriving Show | ||
840 | data instance Transaction Response = TransactionR | ||
841 | { transIdR :: {-# UNPACK #-} !TransactionId | ||
842 | , response :: !Response | ||
843 | } deriving Show | ||
844 | |||
845 | -- TODO newtype | ||
846 | newtype MessageId = MessageId Word32 | ||
847 | deriving (Show, Eq, Num, Serialize) | ||
848 | |||
849 | connectId, announceId, scrapeId, errorId :: MessageId | ||
850 | connectId = 0 | ||
851 | announceId = 1 | ||
852 | scrapeId = 2 | ||
853 | errorId = 3 | ||
854 | |||
855 | instance Serialize (Transaction Request) where | ||
856 | put TransactionQ {..} = do | ||
857 | case request of | ||
858 | Connect -> do | ||
859 | put initialConnectionId | ||
860 | put connectId | ||
861 | put transIdQ | ||
862 | |||
863 | Announce ann -> do | ||
864 | put connIdQ | ||
865 | put announceId | ||
866 | put transIdQ | ||
867 | put ann | ||
868 | |||
869 | Scrape hashes -> do | ||
870 | put connIdQ | ||
871 | put scrapeId | ||
872 | put transIdQ | ||
873 | forM_ hashes put | ||
874 | |||
875 | get = do | ||
876 | cid <- get | ||
877 | mid <- get | ||
878 | TransactionQ cid <$> S.get <*> getBody mid | ||
879 | where | ||
880 | getBody :: MessageId -> S.Get Request | ||
881 | getBody msgId | ||
882 | | msgId == connectId = pure Connect | ||
883 | | msgId == announceId = Announce <$> get | ||
884 | | msgId == scrapeId = Scrape <$> many get | ||
885 | | otherwise = fail errMsg | ||
886 | where | ||
887 | errMsg = "unknown request: " ++ show msgId | ||
888 | |||
889 | instance Serialize (Transaction Response) where | ||
890 | put TransactionR {..} = do | ||
891 | case response of | ||
892 | Connected conn -> do | ||
893 | put connectId | ||
894 | put transIdR | ||
895 | put conn | ||
896 | |||
897 | Announced info -> do | ||
898 | put announceId | ||
899 | put transIdR | ||
900 | put info | ||
901 | |||
902 | Scraped infos -> do | ||
903 | put scrapeId | ||
904 | put transIdR | ||
905 | forM_ infos put | ||
906 | |||
907 | Failed info -> do | ||
908 | put errorId | ||
909 | put transIdR | ||
910 | put (encodeUtf8 info) | ||
911 | |||
912 | |||
913 | get = do | ||
914 | mid <- get | ||
915 | TransactionR <$> get <*> getBody mid | ||
916 | where | ||
917 | getBody :: MessageId -> S.Get Response | ||
918 | getBody msgId | ||
919 | | msgId == connectId = Connected <$> get | ||
920 | | msgId == announceId = Announced <$> get | ||
921 | | msgId == scrapeId = Scraped <$> many get | ||
922 | | msgId == errorId = (Failed . decodeUtf8) <$> get | ||
923 | | otherwise = fail msg | ||
924 | where | ||
925 | msg = "unknown response: " ++ show msgId | ||
diff --git a/dht/bittorrent/src/Network/BitTorrent/Tracker/RPC.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker/RPC.hs new file mode 100644 index 00000000..45fef05e --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Tracker/RPC/HTTP.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker/RPC/HTTP.hs new file mode 100644 index 00000000..6f7a53bf --- /dev/null +++ b/dht/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 defaultRequest 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/dht/bittorrent/src/Network/BitTorrent/Tracker/RPC/UDP.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker/RPC/UDP.hs new file mode 100644 index 00000000..31b6b870 --- /dev/null +++ b/dht/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/dht/bittorrent/src/Network/BitTorrent/Tracker/Session.hs b/dht/bittorrent/src/Network/BitTorrent/Tracker/Session.hs new file mode 100644 index 00000000..db6ebaff --- /dev/null +++ b/dht/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] | ||
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]) | ||
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] | ||
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/dht/bittorrent/src/System/Torrent/FileMap.hs b/dht/bittorrent/src/System/Torrent/FileMap.hs new file mode 100644 index 00000000..38c475e8 --- /dev/null +++ b/dht/bittorrent/src/System/Torrent/FileMap.hs | |||
@@ -0,0 +1,163 @@ | |||
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 -- XXX: mutable buffer (see 'writeBytes'). | ||
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 | -- Unsafe: FileMap 'writeBytes' will modify supplied bytestrings in place. | ||
66 | fromLazyByteString :: BL.ByteString -> FileMap | ||
67 | fromLazyByteString lbs = V.unfoldr f (0, lbs) | ||
68 | where | ||
69 | f (_, Empty ) = Nothing | ||
70 | f (pos, Chunk x xs) = Just (FileEntry pos x, ((pos + chunkSize), xs)) | ||
71 | where chunkSize = fromIntegral $ BS.length x | ||
72 | |||
73 | -- | /O(n)/. | ||
74 | -- | ||
75 | -- Unsafe: mutable buffers are returned without copy. | ||
76 | toLazyByteString :: FileMap -> BL.ByteString | ||
77 | toLazyByteString = V.foldr f Empty | ||
78 | where | ||
79 | f FileEntry {..} bs = Chunk fileBytes bs | ||
80 | |||
81 | -- | /O(1)/. | ||
82 | size :: FileMap -> FileOffset | ||
83 | size m | ||
84 | | V.null m = 0 | ||
85 | | FileEntry {..} <- V.unsafeLast m | ||
86 | = filePosition + fromIntegral (BS.length fileBytes) | ||
87 | |||
88 | -- | Find the file number for a particular byte offset within a torrent. | ||
89 | bsearch :: FileOffset -> FileMap -> Maybe Int | ||
90 | bsearch x m | ||
91 | | V.null m = Nothing | ||
92 | | otherwise = branch (V.length m `div` 2) | ||
93 | where | ||
94 | branch c @ ((m !) -> FileEntry {..}) | ||
95 | | x < filePosition = bsearch x (V.take c m) | ||
96 | | x >= filePosition + fileSize = do | ||
97 | ix <- bsearch x (V.drop (succ c) m) | ||
98 | return $ succ c + ix | ||
99 | | otherwise = Just c | ||
100 | where | ||
101 | fileSize = fromIntegral (BS.length fileBytes) | ||
102 | |||
103 | -- | /O(log n)/. | ||
104 | drop :: FileOffset -> FileMap -> (FileSize, FileMap) | ||
105 | drop off m | ||
106 | | Just ix <- bsearch off m | ||
107 | , FileEntry {..} <- m ! ix = (off - filePosition, V.drop ix m) | ||
108 | | otherwise = (0 , V.empty) | ||
109 | |||
110 | -- | /O(log n)/. | ||
111 | take :: FileSize -> FileMap -> (FileMap, FileSize) | ||
112 | take len m | ||
113 | | len >= s = (m , 0) | ||
114 | | Just ix <- bsearch (pred len) m = let m' = V.take (succ ix) m | ||
115 | in (m', System.Torrent.FileMap.size m' - len) | ||
116 | | otherwise = (V.empty , 0) | ||
117 | where | ||
118 | s = System.Torrent.FileMap.size m | ||
119 | |||
120 | -- | /O(log n + m)/. Do not use this function with 'unmapFiles'. | ||
121 | -- | ||
122 | -- The returned bytestring points directly into an area memory mapped from a | ||
123 | -- file. | ||
124 | unsafeReadBytes :: FileOffset -> FileSize -> FileMap -> BL.ByteString | ||
125 | unsafeReadBytes off s m | ||
126 | | (l , m') <- System.Torrent.FileMap.drop off m | ||
127 | , (m'', _ ) <- System.Torrent.FileMap.take (off + s) m' | ||
128 | = BL.take (fromIntegral s) $ BL.drop (fromIntegral l) $ toLazyByteString m'' | ||
129 | |||
130 | -- The returned bytestring is copied and safe to use after the file is | ||
131 | -- unmapped. | ||
132 | readBytes :: FileOffset -> FileSize -> FileMap -> IO BL.ByteString | ||
133 | readBytes off s m = do | ||
134 | let bs_copy = BL.copy $ unsafeReadBytes off s m | ||
135 | forceLBS bs_copy | ||
136 | return bs_copy | ||
137 | where | ||
138 | forceLBS Empty = return () | ||
139 | forceLBS (Chunk _ x) = forceLBS x | ||
140 | |||
141 | -- UNSAFE: Uses the first byte string as a pointer to mutable data and writes | ||
142 | -- the contents of the second bytestring there. | ||
143 | bscpy :: BL.ByteString -> BL.ByteString -> IO () | ||
144 | bscpy (PS _ _ 0 `Chunk` dest_rest) src = bscpy dest_rest src | ||
145 | bscpy dest (PS _ _ 0 `Chunk` src_rest) = bscpy dest src_rest | ||
146 | bscpy (PS dest_fptr dest_off dest_size `Chunk` dest_rest) | ||
147 | (PS src_fptr src_off src_size `Chunk` src_rest) | ||
148 | = do let csize = min dest_size src_size | ||
149 | withForeignPtr dest_fptr $ \dest_ptr -> | ||
150 | withForeignPtr src_fptr $ \src_ptr -> | ||
151 | memcpy (dest_ptr `advancePtr` dest_off) | ||
152 | (src_ptr `advancePtr` src_off) | ||
153 | (fromIntegral csize) -- TODO memmove? | ||
154 | bscpy (PS dest_fptr (dest_off + csize) (dest_size - csize) `Chunk` dest_rest) | ||
155 | (PS src_fptr (src_off + csize) (src_size - csize) `Chunk` src_rest) | ||
156 | bscpy _ _ = return () | ||
157 | |||
158 | -- UNSAFE: Mutates bytestring contents within the provided FileMap. | ||
159 | writeBytes :: FileOffset -> BL.ByteString -> FileMap -> IO () | ||
160 | writeBytes off lbs m = bscpy dest src | ||
161 | where | ||
162 | src = BL.take (fromIntegral (BL.length dest)) lbs | ||
163 | dest = unsafeReadBytes off (fromIntegral (BL.length lbs)) m | ||
diff --git a/dht/bittorrent/src/System/Torrent/Storage.hs b/dht/bittorrent/src/System/Torrent/Storage.hs new file mode 100644 index 00000000..1d77e55d --- /dev/null +++ b/dht/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/dht/bittorrent/src/System/Torrent/Tree.hs b/dht/bittorrent/src/System/Torrent/Tree.hs new file mode 100644 index 00000000..41cfb360 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Config.hs b/dht/bittorrent/tests/Config.hs new file mode 100644 index 00000000..55e30867 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Data/TorrentSpec.hs b/dht/bittorrent/tests/Data/TorrentSpec.hs new file mode 100644 index 00000000..b4a280e4 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Main.hs b/dht/bittorrent/tests/Main.hs new file mode 100644 index 00000000..5ed953da --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Client/HandleSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Client/HandleSpec.hs new file mode 100644 index 00000000..d51bab02 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/CoreSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/CoreSpec.hs new file mode 100644 index 00000000..e9b17a42 --- /dev/null +++ b/dht/bittorrent/tests/Network/BitTorrent/CoreSpec.hs | |||
@@ -0,0 +1,309 @@ | |||
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 | #if MIN_VERSION_iproute(1,7,4) | ||
8 | import Data.IP hiding (fromSockAddr) | ||
9 | #else | ||
10 | import Data.IP | ||
11 | #endif | ||
12 | import Data.Serialize as S | ||
13 | import Data.String | ||
14 | import Data.Text.Encoding as T | ||
15 | import Data.Word | ||
16 | import Network | ||
17 | import Test.Hspec | ||
18 | import Test.QuickCheck | ||
19 | import Test.QuickCheck.Instances () | ||
20 | |||
21 | import Network.BitTorrent.Address | ||
22 | |||
23 | |||
24 | instance Arbitrary IPv4 where | ||
25 | arbitrary = do | ||
26 | a <- choose (0, 255) | ||
27 | b <- choose (0, 255) | ||
28 | c <- choose (0, 255) | ||
29 | d <- choose (0, 255) | ||
30 | return $ toIPv4 [a, b, c, d] | ||
31 | |||
32 | instance Arbitrary IPv6 where | ||
33 | arbitrary = do | ||
34 | a <- choose (0, fromIntegral (maxBound :: Word16)) | ||
35 | b <- choose (0, fromIntegral (maxBound :: Word16)) | ||
36 | c <- choose (0, fromIntegral (maxBound :: Word16)) | ||
37 | d <- choose (0, fromIntegral (maxBound :: Word16)) | ||
38 | e <- choose (0, fromIntegral (maxBound :: Word16)) | ||
39 | f <- choose (0, fromIntegral (maxBound :: Word16)) | ||
40 | g <- choose (0, fromIntegral (maxBound :: Word16)) | ||
41 | h <- choose (0, fromIntegral (maxBound :: Word16)) | ||
42 | return $ toIPv6 [a, b, c, d, e, f, g, h] | ||
43 | |||
44 | instance Arbitrary IP where | ||
45 | arbitrary = frequency | ||
46 | [ (1, IPv4 <$> arbitrary) | ||
47 | , (1, IPv6 <$> arbitrary) | ||
48 | ] | ||
49 | |||
50 | instance Arbitrary PortNumber where | ||
51 | arbitrary = fromIntegral <$> (arbitrary :: Gen Word16) | ||
52 | |||
53 | instance Arbitrary PeerId where | ||
54 | arbitrary = oneof | ||
55 | [ azureusStyle defaultClientId defaultVersionNumber | ||
56 | <$> (T.encodeUtf8 <$> arbitrary) | ||
57 | , shadowStyle 'X' defaultVersionNumber | ||
58 | <$> (T.encodeUtf8 <$> arbitrary) | ||
59 | ] | ||
60 | |||
61 | instance Arbitrary a => Arbitrary (PeerAddr a) where | ||
62 | arbitrary = PeerAddr <$> arbitrary <*> arbitrary <*> arbitrary | ||
63 | |||
64 | instance Arbitrary NodeId where | ||
65 | arbitrary = fromString <$> vector 20 | ||
66 | |||
67 | instance Arbitrary a => Arbitrary (NodeAddr a) where | ||
68 | arbitrary = NodeAddr <$> arbitrary <*> arbitrary | ||
69 | |||
70 | instance Arbitrary a => Arbitrary (NodeInfo a) where | ||
71 | arbitrary = NodeInfo <$> arbitrary <*> arbitrary | ||
72 | |||
73 | spec :: Spec | ||
74 | spec = do | ||
75 | describe "PeerId" $ do | ||
76 | it "properly bencoded" $ do | ||
77 | BE.decode "20:01234567890123456789" | ||
78 | `shouldBe` Right ("01234567890123456789" :: PeerId) | ||
79 | |||
80 | describe "PortNumber" $ do | ||
81 | it "properly serialized" $ do | ||
82 | S.decode "\x1\x2" `shouldBe` Right (258 :: PortNumber) | ||
83 | S.encode (258 :: PortNumber) `shouldBe` "\x1\x2" | ||
84 | |||
85 | it "properly bencoded" $ do | ||
86 | BE.decode "i80e" `shouldBe` Right (80 :: PortNumber) | ||
87 | |||
88 | it "fail if port number is invalid" $ do | ||
89 | (BE.decode "i-10e" :: BE.Result PortNumber) | ||
90 | `shouldBe` | ||
91 | Left "fromBEncode: unable to decode PortNumber: -10" | ||
92 | |||
93 | (BE.decode "i70000e" :: BE.Result PortNumber) | ||
94 | `shouldBe` | ||
95 | Left "fromBEncode: unable to decode PortNumber: 70000" | ||
96 | |||
97 | describe "Peer IPv4" $ do | ||
98 | it "properly serialized" $ do | ||
99 | S.decode "\x1\x2\x3\x4" `shouldBe` Right (toIPv4 [1, 2, 3, 4]) | ||
100 | S.encode (toIPv4 [1, 2, 3, 4]) `shouldBe` "\x1\x2\x3\x4" | ||
101 | |||
102 | it "properly serialized (iso)" $ property $ \ ip -> do | ||
103 | S.decode (S.encode ip) `shouldBe` Right (ip :: IPv4) | ||
104 | |||
105 | it "properly bencoded" $ do | ||
106 | BE.decode "11:168.192.0.1" `shouldBe` Right (toIPv4 [168, 192, 0, 1]) | ||
107 | BE.encode (toIPv4 [168, 192, 0, 1]) `shouldBe` "11:168.192.0.1" | ||
108 | |||
109 | it "properly bencoded (iso)" $ property $ \ ip -> | ||
110 | BE.decode (BL.toStrict (BE.encode ip)) `shouldBe` Right (ip :: IPv4) | ||
111 | |||
112 | it "fail gracefully on invalid strings" $ do | ||
113 | BE.decode "3:1.1" `shouldBe` | ||
114 | (Left "fromBEncode: unable to decode IP: 1.1" :: BE.Result IPv4) | ||
115 | |||
116 | it "fail gracefully on invalid bencode" $ do | ||
117 | BE.decode "i10e" `shouldBe` | ||
118 | (Left "fromBEncode: unable to decode IP: addr should be a bstring" | ||
119 | :: BE.Result IPv4) | ||
120 | |||
121 | describe "Peer IPv6" $ do | ||
122 | it "properly serialized" $ do | ||
123 | S.decode "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10" | ||
124 | `shouldBe` | ||
125 | Right ("102:304:506:708:90a:b0c:d0e:f10" :: IPv6) | ||
126 | |||
127 | S.encode ("102:304:506:708:90a:b0c:d0e:f10" :: IPv6) | ||
128 | `shouldBe` | ||
129 | "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10" | ||
130 | |||
131 | it "properly serialized (iso)" $ property $ \ ip -> | ||
132 | S.decode (S.encode ip) `shouldBe` Right (ip :: IPv6) | ||
133 | |||
134 | it "properly bencoded" $ do | ||
135 | BE.decode "3:::1" `shouldBe` Right (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]) | ||
136 | BE.encode (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]) `shouldBe` | ||
137 | "23:00:00:00:00:00:00:00:01" | ||
138 | |||
139 | BE.decode "23:00:00:00:00:00:00:00:01" | ||
140 | `shouldBe` | ||
141 | Right (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1]) | ||
142 | |||
143 | it "properly bencoded iso" $ property $ \ ip -> | ||
144 | BE.decode (BL.toStrict (BE.encode ip)) `shouldBe` Right (ip :: IPv4) | ||
145 | |||
146 | it "fail gracefully on invalid strings" $ do | ||
147 | BE.decode "4:g::1" `shouldBe` | ||
148 | (Left "fromBEncode: unable to decode IP: g::1" :: BE.Result IPv6) | ||
149 | |||
150 | it "fail gracefully on invalid bencode" $ do | ||
151 | BE.decode "i10e" `shouldBe` | ||
152 | (Left "fromBEncode: unable to decode IP: addr should be a bstring" | ||
153 | :: BE.Result IPv6) | ||
154 | |||
155 | |||
156 | describe "Peer IP" $ do | ||
157 | it "properly serialized IPv6" $ do | ||
158 | S.decode "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10" | ||
159 | `shouldBe` | ||
160 | Right ("102:304:506:708:90a:b0c:d0e:f10" :: IP) | ||
161 | |||
162 | S.encode ("102:304:506:708:90a:b0c:d0e:f10" :: IP) | ||
163 | `shouldBe` | ||
164 | "\x1\x2\x3\x4\x5\x6\x7\x8\x9\xa\xb\xc\xd\xe\xf\x10" | ||
165 | |||
166 | it "properly serialized (iso) IPv6" $ property $ \ ip -> | ||
167 | S.decode (S.encode ip) `shouldBe` Right (ip :: IP) | ||
168 | |||
169 | it "properly serialized IPv4" $ do | ||
170 | S.decode "\x1\x2\x3\x4" `shouldBe` Right (IPv4 $ toIPv4 [1, 2, 3, 4]) | ||
171 | S.encode (toIPv4 [1, 2, 3, 4]) `shouldBe` "\x1\x2\x3\x4" | ||
172 | |||
173 | it "properly serialized (iso) IPv4" $ property $ \ ip -> do | ||
174 | S.decode (S.encode ip) `shouldBe` Right (ip :: IP) | ||
175 | |||
176 | it "properly bencoded" $ do | ||
177 | BE.decode "11:168.192.0.1" `shouldBe` | ||
178 | Right (IPv4 (toIPv4 [168, 192, 0, 1])) | ||
179 | |||
180 | BE.decode "3:::1" `shouldBe` Right | ||
181 | (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])) | ||
182 | |||
183 | BE.encode (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])) `shouldBe` | ||
184 | "23:00:00:00:00:00:00:00:01" | ||
185 | |||
186 | BE.decode "23:00:00:00:00:00:00:00:01" | ||
187 | `shouldBe` | ||
188 | Right (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])) | ||
189 | |||
190 | it "properly bencoded iso" $ property $ \ ip -> | ||
191 | BE.decode (BL.toStrict (BE.encode ip)) `shouldBe` Right (ip :: IP) | ||
192 | |||
193 | it "fail gracefully on invalid strings" $ do | ||
194 | BE.decode "4:g::1" `shouldBe` | ||
195 | (Left "fromBEncode: unable to decode IP: g::1" :: BE.Result IP) | ||
196 | |||
197 | it "fail gracefully on invalid bencode" $ do | ||
198 | BE.decode "i10e" `shouldBe` | ||
199 | (Left "fromBEncode: unable to decode IP: addr should be a bstring" | ||
200 | :: BE.Result IP) | ||
201 | |||
202 | describe "PeerAddr" $ do | ||
203 | it "IsString" $ do | ||
204 | ("127.0.0.1:80" :: PeerAddr IP) | ||
205 | `shouldBe` PeerAddr Nothing "127.0.0.1" 80 | ||
206 | |||
207 | ("127.0.0.1:80" :: PeerAddr IPv4) | ||
208 | `shouldBe` PeerAddr Nothing "127.0.0.1" 80 | ||
209 | |||
210 | ("[::1]:80" :: PeerAddr IP) | ||
211 | `shouldBe` PeerAddr Nothing "::1" 80 | ||
212 | |||
213 | ("[::1]:80" :: PeerAddr IPv6) | ||
214 | `shouldBe` PeerAddr Nothing "::1" 80 | ||
215 | |||
216 | it "properly bencoded (iso)" $ property $ \ addr -> | ||
217 | BE.decode (BL.toStrict (BE.encode addr)) | ||
218 | `shouldBe` Right (addr :: PeerAddr IP) | ||
219 | |||
220 | |||
221 | it "properly bencoded (ipv4)" $ do | ||
222 | BE.decode "d2:ip11:168.192.0.1\ | ||
223 | \7:peer id20:01234567890123456789\ | ||
224 | \4:porti6881e\ | ||
225 | \e" | ||
226 | `shouldBe` | ||
227 | Right (PeerAddr (Just "01234567890123456789") | ||
228 | (IPv4 (toIPv4 [168, 192, 0, 1])) | ||
229 | 6881) | ||
230 | |||
231 | it "properly bencoded (ipv6)" $ do | ||
232 | BE.decode "d2:ip3:::1\ | ||
233 | \7:peer id20:01234567890123456789\ | ||
234 | \4:porti6881e\ | ||
235 | \e" | ||
236 | `shouldBe` | ||
237 | Right (PeerAddr (Just "01234567890123456789") | ||
238 | (IPv6 (toIPv6 [0, 0, 0, 0, 0, 0, 0, 1])) | ||
239 | 6881) | ||
240 | |||
241 | it "peer id is optional" $ do | ||
242 | BE.decode "d2:ip11:168.192.0.1\ | ||
243 | \4:porti6881e\ | ||
244 | \e" | ||
245 | `shouldBe` | ||
246 | Right (PeerAddr Nothing (IPv4 (toIPv4 [168, 192, 0, 1])) 6881) | ||
247 | |||
248 | it "has sock addr for both ipv4 and ipv6" $ do | ||
249 | show (peerSockAddr "128.0.0.1:80") `shouldBe` "128.0.0.1:80" | ||
250 | show (peerSockAddr "[::1]:8080" ) `shouldBe` "[::1]:8080" | ||
251 | |||
252 | describe "NodeId" $ do | ||
253 | it "properly serialized" $ do | ||
254 | S.decode "mnopqrstuvwxyz123456" | ||
255 | `shouldBe` Right ("mnopqrstuvwxyz123456" :: NodeId) | ||
256 | |||
257 | S.encode ("mnopqrstuvwxyz123456" :: NodeId) | ||
258 | `shouldBe` "mnopqrstuvwxyz123456" | ||
259 | |||
260 | it "properly serialized (iso)" $ property $ \ nid -> | ||
261 | S.decode (S.encode nid) `shouldBe` | ||
262 | Right (nid :: NodeId) | ||
263 | |||
264 | describe "NodeAddr" $ do | ||
265 | it "properly serialized" $ do | ||
266 | S.decode "\127\0\0\1\1\2" `shouldBe` | ||
267 | Right ("127.0.0.1:258" :: NodeAddr IPv4) | ||
268 | |||
269 | it "properly serialized (iso)" $ property $ \ nid -> | ||
270 | S.decode (S.encode nid) `shouldBe` | ||
271 | Right (nid :: NodeAddr IPv4) | ||
272 | |||
273 | describe "NodeInfo" $ do | ||
274 | it "properly serialized" $ do | ||
275 | S.decode "mnopqrstuvwxyz123456\ | ||
276 | \\127\0\0\1\1\2" `shouldBe` Right | ||
277 | (NodeInfo "mnopqrstuvwxyz123456" "127.0.0.1:258" :: NodeInfo IPv4) | ||
278 | |||
279 | it "properly serialized (iso)" $ property $ \ nid -> | ||
280 | S.decode (S.encode nid) `shouldBe` | ||
281 | Right (nid :: NodeInfo IPv4) | ||
282 | |||
283 | -- see <http://bittorrent.org/beps/bep_0020.html> | ||
284 | describe "Fingerprint" $ do | ||
285 | it "decode mainline encoded peer id" $ do | ||
286 | fingerprint "M4-3-6--xxxxxxxxxxxx" `shouldBe` "Mainline-4.3.6" | ||
287 | fingerprint "M4-20-8-xxxxxxxxxxxx" `shouldBe` "Mainline-4.20.8" | ||
288 | |||
289 | it "decode azureus encoded peer id" $ do | ||
290 | fingerprint "-AZ2060-xxxxxxxxxxxx" `shouldBe` "Azureus-2060" | ||
291 | fingerprint "-BS0000-xxxxxxxxxxxx" `shouldBe` "BTSlave-0" | ||
292 | |||
293 | it "decode Shad0w style peer id" $ do | ||
294 | fingerprint "S58B-----xxxxxxxxxxx" `shouldBe` "Shadow-5.8.11" | ||
295 | fingerprint "T58B-----xxxxxxxxxxx" `shouldBe` "BitTornado-5.8.11" | ||
296 | |||
297 | it "decode bitcomet style peer id" $ do | ||
298 | fingerprint "exbc01xxxxxxxxxxxxxx" `shouldBe` "BitComet-48.49" | ||
299 | fingerprint "FUTB01xxxxxxxxxxxxxx" `shouldBe` "BitComet-48.49" | ||
300 | fingerprint "exbc01LORDxxxxxxxxxx" `shouldBe` "BitLord-48.49" | ||
301 | |||
302 | it "decode opera style peer id" $ do | ||
303 | fingerprint "OP0123xxxxxxxxxxxxxx" `shouldBe` "Opera-123" | ||
304 | |||
305 | it "decode ML donkey style peer id" $ do | ||
306 | fingerprint "-ML2.7.2-xxxxxxxxxxx" `shouldBe` "MLdonkey-0" | ||
307 | |||
308 | -- TODO XBT, Bits on Wheels, Queen Bee, BitTyrant, TorrenTopia, | ||
309 | -- BitSpirit, Rufus, G3 Torrent, FlashGet | ||
diff --git a/dht/bittorrent/tests/Network/BitTorrent/DHT/MessageSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/DHT/MessageSpec.hs new file mode 100644 index 00000000..6f3c7489 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/DHT/QuerySpec.hs b/dht/bittorrent/tests/Network/BitTorrent/DHT/QuerySpec.hs new file mode 100644 index 00000000..93f78263 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/DHT/RoutingSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/DHT/RoutingSpec.hs new file mode 100644 index 00000000..07a906ba --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/DHT/SessionSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/DHT/SessionSpec.hs new file mode 100644 index 00000000..32e4c158 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/DHT/TestData.hs b/dht/bittorrent/tests/Network/BitTorrent/DHT/TestData.hs new file mode 100644 index 00000000..e9473cbb --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/DHT/TokenSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/DHT/TokenSpec.hs new file mode 100644 index 00000000..a45d2212 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/DHTSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/DHTSpec.hs new file mode 100644 index 00000000..77160eb5 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Exchange/BitfieldSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Exchange/BitfieldSpec.hs new file mode 100644 index 00000000..1ba772f6 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Exchange/BlockSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Exchange/BlockSpec.hs new file mode 100644 index 00000000..2dc8e0b8 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Exchange/ConnectionSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Exchange/ConnectionSpec.hs new file mode 100644 index 00000000..d654cda1 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Exchange/DownloadSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Exchange/DownloadSpec.hs new file mode 100644 index 00000000..d46f2034 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Exchange/MessageSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Exchange/MessageSpec.hs new file mode 100644 index 00000000..d615b1ff --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Exchange/SessionSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Exchange/SessionSpec.hs new file mode 100644 index 00000000..bf5b95a1 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Internal/CacheSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Internal/CacheSpec.hs new file mode 100644 index 00000000..337e7add --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Internal/ProgressSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Internal/ProgressSpec.hs new file mode 100644 index 00000000..acbfd84c --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/ListSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/ListSpec.hs new file mode 100644 index 00000000..bba9d0e2 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/MessageSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/MessageSpec.hs new file mode 100644 index 00000000..29854d58 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/RPC/HTTPSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/RPC/HTTPSpec.hs new file mode 100644 index 00000000..e928f917 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/RPC/UDPSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/RPC/UDPSpec.hs new file mode 100644 index 00000000..73acb3fa --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/RPCSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/RPCSpec.hs new file mode 100644 index 00000000..dfc13a1e --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/SessionSpec.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/SessionSpec.hs new file mode 100644 index 00000000..72936ee7 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/BitTorrent/Tracker/TestData.hs b/dht/bittorrent/tests/Network/BitTorrent/Tracker/TestData.hs new file mode 100644 index 00000000..b95e2df4 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/KRPC/MessageSpec.hs b/dht/bittorrent/tests/Network/KRPC/MessageSpec.hs new file mode 100644 index 00000000..498ef679 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/KRPC/MethodSpec.hs b/dht/bittorrent/tests/Network/KRPC/MethodSpec.hs new file mode 100644 index 00000000..c1c58282 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Network/KRPCSpec.hs b/dht/bittorrent/tests/Network/KRPCSpec.hs new file mode 100644 index 00000000..eabcc817 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Readme.md b/dht/bittorrent/tests/Readme.md new file mode 100644 index 00000000..7a9d8914 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/Spec.hs b/dht/bittorrent/tests/Spec.hs new file mode 100644 index 00000000..b4e92e75 --- /dev/null +++ b/dht/bittorrent/tests/Spec.hs | |||
@@ -0,0 +1 @@ | |||
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-} | |||
diff --git a/dht/bittorrent/tests/System/Torrent/FileMapSpec.hs b/dht/bittorrent/tests/System/Torrent/FileMapSpec.hs new file mode 100644 index 00000000..29252925 --- /dev/null +++ b/dht/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/dht/bittorrent/tests/System/Torrent/StorageSpec.hs b/dht/bittorrent/tests/System/Torrent/StorageSpec.hs new file mode 100644 index 00000000..b5e49078 --- /dev/null +++ b/dht/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 | ||