diff options
-rw-r--r-- | network-bittorrent.cabal | 2 | ||||
-rw-r--r-- | src/Data/Torrent.hs | 9 | ||||
-rw-r--r-- | src/Data/Torrent/InfoHash.hs | 41 | ||||
-rw-r--r-- | src/Network/Torrent/PeerWire/Handshake.hs | 12 | ||||
-rw-r--r-- | src/Network/Torrent/Tracker.hs | 9 | ||||
-rw-r--r-- | tests/encoding.hs | 25 | ||||
-rw-r--r-- | tests/info-hash.hs | 9 |
7 files changed, 82 insertions, 25 deletions
diff --git a/network-bittorrent.cabal b/network-bittorrent.cabal index 903c7ae7..399b0c7c 100644 --- a/network-bittorrent.cabal +++ b/network-bittorrent.cabal | |||
@@ -18,6 +18,7 @@ source-repository head | |||
18 | 18 | ||
19 | library | 19 | library |
20 | exposed-modules: Data.Torrent | 20 | exposed-modules: Data.Torrent |
21 | , Data.Torrent.InfoHash | ||
21 | , Network.Torrent | 22 | , Network.Torrent |
22 | , Network.Torrent.PeerID | 23 | , Network.Torrent.PeerID |
23 | , Network.Torrent.Tracker | 24 | , Network.Torrent.Tracker |
@@ -101,6 +102,7 @@ test-suite encoding | |||
101 | , test-framework | 102 | , test-framework |
102 | , test-framework-quickcheck2 | 103 | , test-framework-quickcheck2 |
103 | , QuickCheck | 104 | , QuickCheck |
105 | , network >= 2.4.0.13 | ||
104 | , network-bittorrent >= 0.1.0.0 | 106 | , network-bittorrent >= 0.1.0.0 |
105 | 107 | ||
106 | ghc-options: -Wall -fno-warn-orphans | 108 | ghc-options: -Wall -fno-warn-orphans |
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs index 128c1c7c..57831acf 100644 --- a/src/Data/Torrent.hs +++ b/src/Data/Torrent.hs | |||
@@ -2,7 +2,8 @@ | |||
2 | {-# LANGUAGE OverloadedStrings #-} | 2 | {-# LANGUAGE OverloadedStrings #-} |
3 | -- | This module provides torrent metainfo serialization. | 3 | -- | This module provides torrent metainfo serialization. |
4 | module Data.Torrent | 4 | module Data.Torrent |
5 | ( Torrent(..), TorrentInfo(..), TorrentFile(..) | 5 | ( module Data.Torrent.InfoHash |
6 | , Torrent(..), TorrentInfo(..), TorrentFile(..) | ||
6 | , fromFile | 7 | , fromFile |
7 | ) where | 8 | ) where |
8 | 9 | ||
@@ -13,8 +14,8 @@ import Data.ByteString (ByteString) | |||
13 | import qualified Data.ByteString as B | 14 | import qualified Data.ByteString as B |
14 | import qualified Data.ByteString.Char8 as BC (pack, unpack) | 15 | import qualified Data.ByteString.Char8 as BC (pack, unpack) |
15 | import Data.Text (Text) | 16 | import Data.Text (Text) |
16 | import Crypto.Hash.SHA1 | ||
17 | import Data.BEncode | 17 | import Data.BEncode |
18 | import Data.Torrent.InfoHash | ||
18 | import Network.URI | 19 | import Network.URI |
19 | 20 | ||
20 | type Time = Text | 21 | type Time = Text |
@@ -22,7 +23,7 @@ type Time = Text | |||
22 | -- TODO comment fields | 23 | -- TODO comment fields |
23 | -- TODO more convenient form of torrent info. | 24 | -- TODO more convenient form of torrent info. |
24 | data Torrent = Torrent { | 25 | data Torrent = Torrent { |
25 | tInfoHash :: ByteString | 26 | tInfoHash :: InfoHash |
26 | , tAnnounce :: URI | 27 | , tAnnounce :: URI |
27 | , tAnnounceList :: Maybe [[URI]] | 28 | , tAnnounceList :: Maybe [[URI]] |
28 | , tComment :: Maybe Text | 29 | , tComment :: Maybe Text |
@@ -148,4 +149,4 @@ instance BEncodable TorrentFile where | |||
148 | 149 | ||
149 | 150 | ||
150 | fromFile :: FilePath -> IO (Result Torrent) | 151 | fromFile :: FilePath -> IO (Result Torrent) |
151 | fromFile path = (fromBEncode <=< decode) <$> B.readFile path | 152 | fromFile filepath = (fromBEncode <=< decode) <$> B.readFile filepath |
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs new file mode 100644 index 00000000..448e9a5a --- /dev/null +++ b/src/Data/Torrent/InfoHash.hs | |||
@@ -0,0 +1,41 @@ | |||
1 | module Data.Torrent.InfoHash | ||
2 | ( InfoHash (getInfoHash) | ||
3 | |||
4 | -- ^ Construction | ||
5 | , hash, hashlazy | ||
6 | |||
7 | -- ^ Extra | ||
8 | , ppHex | ||
9 | ) where | ||
10 | |||
11 | import Control.Applicative | ||
12 | import Data.Foldable | ||
13 | import Data.ByteString (ByteString) | ||
14 | import qualified Data.ByteString as B | ||
15 | import qualified Data.ByteString.Char8 as BC | ||
16 | import qualified Data.ByteString.Builder as B | ||
17 | import qualified Data.ByteString.Builder.Prim as B | ||
18 | import qualified Data.ByteString.Lazy as Lazy | ||
19 | import Data.Serialize | ||
20 | import qualified Crypto.Hash.SHA1 as C | ||
21 | |||
22 | -- | Exactly 20 bytes long SHA1 hash. | ||
23 | newtype InfoHash = InfoHash { getInfoHash :: ByteString } | ||
24 | deriving (Eq, Ord) | ||
25 | |||
26 | instance Show InfoHash where | ||
27 | show = BC.unpack . ppHex | ||
28 | |||
29 | instance Serialize InfoHash where | ||
30 | put = putByteString . getInfoHash | ||
31 | get = InfoHash <$> getBytes 20 | ||
32 | |||
33 | hash :: ByteString -> InfoHash | ||
34 | hash = InfoHash . C.hash | ||
35 | |||
36 | hashlazy :: Lazy.ByteString -> InfoHash | ||
37 | hashlazy = InfoHash . C.hashlazy | ||
38 | |||
39 | ppHex :: InfoHash -> ByteString | ||
40 | ppHex = Lazy.toStrict . B.toLazyByteString . | ||
41 | foldMap (B.primFixed B.word8HexFixed) . B.unpack . getInfoHash | ||
diff --git a/src/Network/Torrent/PeerWire/Handshake.hs b/src/Network/Torrent/PeerWire/Handshake.hs index 7bdb7fef..ab255c14 100644 --- a/src/Network/Torrent/PeerWire/Handshake.hs +++ b/src/Network/Torrent/PeerWire/Handshake.hs | |||
@@ -9,7 +9,7 @@ import Data.Word | |||
9 | import Data.ByteString (ByteString) | 9 | import Data.ByteString (ByteString) |
10 | import qualified Data.ByteString as B | 10 | import qualified Data.ByteString as B |
11 | import Data.Serialize | 11 | import Data.Serialize |
12 | 12 | import Data.Torrent.InfoHash | |
13 | import Network.Torrent.PeerID | 13 | import Network.Torrent.PeerID |
14 | 14 | ||
15 | -- | In order to establish the connection between peers we should send 'Handshake' | 15 | -- | In order to establish the connection between peers we should send 'Handshake' |
@@ -18,7 +18,7 @@ import Network.Torrent.PeerID | |||
18 | data Handshake = Handshake { | 18 | data Handshake = Handshake { |
19 | hsProtocol :: ByteString -- ^ Identifier of the protocol. | 19 | hsProtocol :: ByteString -- ^ Identifier of the protocol. |
20 | , hsReserved :: Word64 -- ^ Reserved bytes, rarely used. | 20 | , hsReserved :: Word64 -- ^ Reserved bytes, rarely used. |
21 | , hsInfoHash :: ByteString -- ^ Hash from the metainfo file. | 21 | , hsInfoHash :: InfoHash -- ^ Hash from the metainfo file. |
22 | -- This /should be/ same hash that is transmitted in tracker requests. | 22 | -- This /should be/ same hash that is transmitted in tracker requests. |
23 | , hsPeerID :: PeerID -- ^ Peer id of the initiator. | 23 | , hsPeerID :: PeerID -- ^ Peer id of the initiator. |
24 | -- This is /usually the same peer id that is transmitted in tracker requests. | 24 | -- This is /usually the same peer id that is transmitted in tracker requests. |
@@ -29,14 +29,14 @@ instance Serialize Handshake where | |||
29 | putWord8 (fromIntegral (B.length (hsProtocol hs))) | 29 | putWord8 (fromIntegral (B.length (hsProtocol hs))) |
30 | putByteString (hsProtocol hs) | 30 | putByteString (hsProtocol hs) |
31 | putWord64be (hsReserved hs) | 31 | putWord64be (hsReserved hs) |
32 | putByteString (hsInfoHash hs) | 32 | put (hsInfoHash hs) |
33 | put (hsPeerID hs) | 33 | put (hsPeerID hs) |
34 | 34 | ||
35 | get = do | 35 | get = do |
36 | len <- getWord8 | 36 | len <- getWord8 |
37 | Handshake <$> getBytes (fromIntegral len) | 37 | Handshake <$> getBytes (fromIntegral len) |
38 | <*> getWord64be | 38 | <*> getWord64be |
39 | <*> getBytes 20 | 39 | <*> get |
40 | <*> get | 40 | <*> get |
41 | 41 | ||
42 | -- | Default protocol string "BitTorrent protocol" as is. | 42 | -- | Default protocol string "BitTorrent protocol" as is. |
@@ -48,7 +48,5 @@ defaultReserved :: Word64 | |||
48 | defaultReserved = 0 | 48 | defaultReserved = 0 |
49 | 49 | ||
50 | -- | Length of info hash and peer id is unchecked, so it /should/ be equal 20. | 50 | -- | Length of info hash and peer id is unchecked, so it /should/ be equal 20. |
51 | defaultHandshake :: ByteString -- ^ Info hash string. | 51 | defaultHandshake :: InfoHash -> PeerID -> Handshake |
52 | -> PeerID | ||
53 | -> Handshake | ||
54 | defaultHandshake hash pid = Handshake defaultProtocol defaultReserved hash pid \ No newline at end of file | 52 | defaultHandshake hash pid = Handshake defaultProtocol defaultReserved hash pid \ No newline at end of file |
diff --git a/src/Network/Torrent/Tracker.hs b/src/Network/Torrent/Tracker.hs index 72fbcb44..796db66a 100644 --- a/src/Network/Torrent/Tracker.hs +++ b/src/Network/Torrent/Tracker.hs | |||
@@ -22,6 +22,7 @@ import Data.ByteString.Char8 as BC | |||
22 | import Data.Text as T | 22 | import Data.Text as T |
23 | import Data.Serialize.Get hiding (Result) | 23 | import Data.Serialize.Get hiding (Result) |
24 | import Data.URLEncoded as URL | 24 | import Data.URLEncoded as URL |
25 | import Data.Torrent | ||
25 | 26 | ||
26 | import Network | 27 | import Network |
27 | import Network.Socket | 28 | import Network.Socket |
@@ -31,8 +32,6 @@ import Network.Torrent.PeerID | |||
31 | 32 | ||
32 | import Numeric | 33 | import Numeric |
33 | 34 | ||
34 | type Hash = ByteString | ||
35 | |||
36 | data Peer = Peer { | 35 | data Peer = Peer { |
37 | peerID :: Maybe PeerID | 36 | peerID :: Maybe PeerID |
38 | , peerIP :: HostAddress | 37 | , peerIP :: HostAddress |
@@ -46,7 +45,7 @@ data Event = Started -- ^ For first request. | |||
46 | 45 | ||
47 | data TRequest = TRequest { -- TODO peer here -- TODO detach announce | 46 | data TRequest = TRequest { -- TODO peer here -- TODO detach announce |
48 | reqAnnounce :: URI -- ^ Announce url of the torrent. | 47 | reqAnnounce :: URI -- ^ Announce url of the torrent. |
49 | , reqInfoHash :: Hash -- ^ Hash of info part of the torrent. | 48 | , reqInfoHash :: InfoHash -- ^ Hash of info part of the torrent. |
50 | , reqPeerID :: PeerID -- ^ Id of the peer doing request. () | 49 | , reqPeerID :: PeerID -- ^ Id of the peer doing request. () |
51 | , reqPort :: PortNumber -- ^ Port to listen to for connection from other peers. | 50 | , reqPort :: PortNumber -- ^ Port to listen to for connection from other peers. |
52 | , reqUploaded :: Int -- ^ # of bytes that the peer has uploaded in the swarm. | 51 | , reqUploaded :: Int -- ^ # of bytes that the peer has uploaded in the swarm. |
@@ -155,7 +154,7 @@ instance URLEncode TRequest where | |||
155 | 154 | ||
156 | encodeRequest :: TRequest -> URI | 155 | encodeRequest :: TRequest -> URI |
157 | encodeRequest req = URL.urlEncode req `addToURI` reqAnnounce req | 156 | encodeRequest req = URL.urlEncode req `addToURI` reqAnnounce req |
158 | `addHash` BC.unpack (reqInfoHash req) | 157 | `addHash` BC.unpack (getInfoHash (reqInfoHash req)) |
159 | where | 158 | where |
160 | addHash :: URI -> String -> URI | 159 | addHash :: URI -> String -> URI |
161 | addHash uri s = uri { uriQuery = uriQuery uri ++ "&info_hash=" ++ rfc1738Encode s } | 160 | addHash uri s = uri { uriQuery = uriQuery uri ++ "&info_hash=" ++ rfc1738Encode s } |
@@ -176,7 +175,7 @@ encodeRequest req = URL.urlEncode req `addToURI` reqAnnounce req | |||
176 | defaultPorts :: [PortNumber] | 175 | defaultPorts :: [PortNumber] |
177 | defaultPorts = [6881..6889] | 176 | defaultPorts = [6881..6889] |
178 | 177 | ||
179 | defaultRequest :: URI -> Hash -> PeerID -> TRequest | 178 | defaultRequest :: URI -> InfoHash -> PeerID -> TRequest |
180 | defaultRequest announce hash pid = | 179 | defaultRequest announce hash pid = |
181 | TRequest { | 180 | TRequest { |
182 | reqAnnounce = announce | 181 | reqAnnounce = announce |
diff --git a/tests/encoding.hs b/tests/encoding.hs index b820dc26..e1a8b63e 100644 --- a/tests/encoding.hs +++ b/tests/encoding.hs | |||
@@ -11,7 +11,9 @@ import Test.Framework (defaultMain) | |||
11 | import Test.Framework.Providers.QuickCheck2 (testProperty) | 11 | import Test.Framework.Providers.QuickCheck2 (testProperty) |
12 | import Test.QuickCheck | 12 | import Test.QuickCheck |
13 | 13 | ||
14 | import Data.Torrent | ||
14 | import Network.Torrent | 15 | import Network.Torrent |
16 | import Network.URI | ||
15 | 17 | ||
16 | positive :: Gen Int | 18 | positive :: Gen Int |
17 | positive = fromIntegral <$> (arbitrary :: Gen Word32) | 19 | positive = fromIntegral <$> (arbitrary :: Gen Word32) |
@@ -45,16 +47,30 @@ instance Arbitrary PeerID where | |||
45 | <*> arbitrary | 47 | <*> arbitrary |
46 | <*> arbitrary | 48 | <*> arbitrary |
47 | 49 | ||
50 | instance Arbitrary InfoHash where | ||
51 | arbitrary = (hash . B.pack) <$> vectorOf 20 arbitrary | ||
52 | |||
48 | instance Arbitrary Handshake where | 53 | instance Arbitrary Handshake where |
49 | arbitrary = defaultHandshake | 54 | arbitrary = defaultHandshake <$> arbitrary <*> arbitrary |
50 | <$> (B.pack <$> (vectorOf 20 arbitrary)) | ||
51 | <*> arbitrary | ||
52 | 55 | ||
53 | data T a = T | 56 | data T a = T |
54 | 57 | ||
55 | prop_encoding :: (Serialize a, Eq a) => T a -> [a] -> Bool | 58 | prop_encoding :: (Serialize a, Eq a) => T a -> [a] -> Bool |
56 | prop_encoding _ msgs = decode (encode msgs) == Right msgs | 59 | prop_encoding _ msgs = decode (encode msgs) == Right msgs |
57 | 60 | ||
61 | test_scrape_url :: Bool | ||
62 | test_scrape_url = check `all` tests | ||
63 | where | ||
64 | check (iu, ou) = (parseURI iu >>= (`scrapeURL` []) >>= return . show) == ou | ||
65 | tests = | ||
66 | [ ("http://example.com/announce" , Just "http://example.com/scrape") | ||
67 | , ("http://example.com/x/announce" , Just "http://example.com/x/scrape") | ||
68 | , ("http://example.com/announce.php" , Just "http://example.com/scrape.php") | ||
69 | , ("http://example.com/a" , Nothing) | ||
70 | , ("http://example.com/announce?x2%0644", Just "http://example.com/scrape?x2%0644") | ||
71 | , ("http://example.com/announce?x=2/4" , Nothing) | ||
72 | , ("http://example.com/x%064announce" , Nothing) | ||
73 | ] | ||
58 | 74 | ||
59 | main :: IO () | 75 | main :: IO () |
60 | main = do | 76 | main = do |
@@ -67,4 +83,7 @@ main = do | |||
67 | 83 | ||
68 | , testProperty "Handshake encode <-> decode" $ | 84 | , testProperty "Handshake encode <-> decode" $ |
69 | prop_encoding (T :: T Handshake) | 85 | prop_encoding (T :: T Handshake) |
86 | |||
87 | , testProperty "Scrape URL" $ | ||
88 | test_scrape_url | ||
70 | ] \ No newline at end of file | 89 | ] \ No newline at end of file |
diff --git a/tests/info-hash.hs b/tests/info-hash.hs index 07c01c9c..1638a205 100644 --- a/tests/info-hash.hs +++ b/tests/info-hash.hs | |||
@@ -1,23 +1,20 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | 1 | {-# LANGUAGE OverloadedStrings #-} |
2 | module Main (main) where | 2 | module Main (main) where |
3 | 3 | ||
4 | import Data.Foldable | ||
5 | import Data.ByteString (ByteString) | 4 | import Data.ByteString (ByteString) |
6 | import qualified Data.ByteString as B | 5 | import qualified Data.ByteString as B |
7 | import qualified Data.ByteString.Char8 as BC | 6 | import qualified Data.ByteString.Char8 as BC |
8 | import qualified Data.ByteString.Lazy as L | 7 | import qualified Data.ByteString.Lazy as L |
9 | import qualified Data.ByteString.Builder as B | 8 | |
10 | import qualified Data.ByteString.Builder.Prim as B | ||
11 | import Data.Torrent | 9 | import Data.Torrent |
10 | import Data.Torrent.InfoHash | ||
11 | |||
12 | import System.Environment | 12 | import System.Environment |
13 | import System.Exit | 13 | import System.Exit |
14 | 14 | ||
15 | checkInfo :: ByteString | 15 | checkInfo :: ByteString |
16 | checkInfo = "0221caf96aa3cb94f0f58d458e78b0fc344ad8bf" | 16 | checkInfo = "0221caf96aa3cb94f0f58d458e78b0fc344ad8bf" |
17 | 17 | ||
18 | ppHex :: B.ByteString -> B.ByteString | ||
19 | ppHex = L.toStrict . B.toLazyByteString . foldMap (B.primFixed B.word8HexFixed) . B.unpack | ||
20 | |||
21 | torrentFileName :: String | 18 | torrentFileName :: String |
22 | torrentFileName = "tests/dapper-dvd-amd64.iso.torrent" | 19 | torrentFileName = "tests/dapper-dvd-amd64.iso.torrent" |
23 | 20 | ||