summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Data/Torrent.hs989
-rw-r--r--src/Data/Torrent/InfoHash.hs164
-rw-r--r--src/Data/Torrent/Layout.hs321
-rw-r--r--src/Data/Torrent/Magnet.hs372
-rw-r--r--src/Data/Torrent/Piece.hs232
-rw-r--r--src/Network/BitTorrent.hs2
-rw-r--r--src/Network/BitTorrent/Address.hs1172
-rw-r--r--src/Network/BitTorrent/Client.hs4
-rw-r--r--src/Network/BitTorrent/Client/Handle.hs2
-rw-r--r--src/Network/BitTorrent/Client/Types.hs6
-rw-r--r--src/Network/BitTorrent/Core.hs88
-rw-r--r--src/Network/BitTorrent/Core/Fingerprint.hs290
-rw-r--r--src/Network/BitTorrent/Core/NodeInfo.hs219
-rw-r--r--src/Network/BitTorrent/Core/PeerAddr.hs354
-rw-r--r--src/Network/BitTorrent/Core/PeerId.hs364
-rw-r--r--src/Network/BitTorrent/DHT.hs5
-rw-r--r--src/Network/BitTorrent/DHT/ContactInfo.hs52
-rw-r--r--src/Network/BitTorrent/DHT/Message.hs4
-rw-r--r--src/Network/BitTorrent/DHT/Query.hs4
-rw-r--r--src/Network/BitTorrent/DHT/Routing.hs4
-rw-r--r--src/Network/BitTorrent/DHT/Session.hs13
-rw-r--r--src/Network/BitTorrent/DHT/Token.hs2
-rw-r--r--src/Network/BitTorrent/Exchange/Assembler.hs168
-rw-r--r--src/Network/BitTorrent/Exchange/Bitfield.hs (renamed from src/Data/Torrent/Bitfield.hs)82
-rw-r--r--src/Network/BitTorrent/Exchange/Block.hs2
-rw-r--r--src/Network/BitTorrent/Exchange/Connection.hs9
-rw-r--r--src/Network/BitTorrent/Exchange/Download.hs295
-rw-r--r--src/Network/BitTorrent/Exchange/Manager.hs4
-rw-r--r--src/Network/BitTorrent/Exchange/Message.hs14
-rw-r--r--src/Network/BitTorrent/Exchange/Selection.hs85
-rw-r--r--src/Network/BitTorrent/Exchange/Session.hs26
-rw-r--r--src/Network/BitTorrent/Exchange/Session/Metadata.hs104
-rw-r--r--src/Network/BitTorrent/Exchange/Session/Status.hs175
-rw-r--r--src/Network/BitTorrent/Internal/Progress.hs (renamed from src/Data/Torrent/Progress.hs)3
-rw-r--r--src/Network/BitTorrent/Tracker/Message.hs7
-rw-r--r--src/Network/BitTorrent/Tracker/RPC.hs8
-rw-r--r--src/Network/BitTorrent/Tracker/RPC/HTTP.hs4
-rw-r--r--src/Network/BitTorrent/Tracker/Session.hs4
-rw-r--r--src/System/Torrent/FileMap.hs2
-rw-r--r--src/System/Torrent/Storage.hs4
-rw-r--r--src/System/Torrent/Tree.hs (renamed from src/Data/Torrent/Tree.hs)10
41 files changed, 2614 insertions, 3055 deletions
diff --git a/src/Data/Torrent.hs b/src/Data/Torrent.hs
index b233937b..cfc26453 100644
--- a/src/Data/Torrent.hs
+++ b/src/Data/Torrent.hs
@@ -17,23 +17,91 @@
17-- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure> 17-- <https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure>
18-- 18--
19{-# LANGUAGE CPP #-} 19{-# LANGUAGE CPP #-}
20{-# LANGUAGE NamedFieldPuns #-}
20{-# LANGUAGE FlexibleInstances #-} 21{-# LANGUAGE FlexibleInstances #-}
21{-# LANGUAGE OverlappingInstances #-} 22{-# LANGUAGE OverlappingInstances #-}
23{-# LANGUAGE MultiParamTypeClasses #-}
22{-# LANGUAGE BangPatterns #-} 24{-# LANGUAGE BangPatterns #-}
23{-# LANGUAGE GeneralizedNewtypeDeriving #-} 25{-# LANGUAGE GeneralizedNewtypeDeriving #-}
26{-# LANGUAGE StandaloneDeriving #-}
24{-# LANGUAGE DeriveDataTypeable #-} 27{-# LANGUAGE DeriveDataTypeable #-}
28{-# LANGUAGE DeriveFunctor #-}
29{-# LANGUAGE DeriveFoldable #-}
30{-# LANGUAGE DeriveTraversable #-}
25{-# LANGUAGE TemplateHaskell #-} 31{-# LANGUAGE TemplateHaskell #-}
26{-# OPTIONS -fno-warn-orphans #-} 32{-# OPTIONS -fno-warn-orphans #-}
27module Data.Torrent 33module Data.Torrent
28 ( -- * Info dictionary 34 ( -- * InfoHash
29 InfoDict (..) 35 -- $infohash
30 , infoDictionary 36 InfoHash
37 , textToInfoHash
38 , longHex
39 , shortHex
31 40
32 -- ** Lenses 41 -- * File layout
42 -- ** FileInfo
43 , FileOffset
44 , FileSize
45 , FileInfo (..)
46 , fileLength
47 , filePath
48 , fileMD5Sum
49
50 -- ** Layout info
51 , LayoutInfo (..)
52 , singleFile
53 , multiFile
54 , rootDirName
55 , joinFilePath
56 , isSingleFile
57 , isMultiFile
58 , suggestedName
59 , contentLength
60 , fileCount
61 , blockCount
62
63 -- ** Flat layout info
64 , FileLayout
65 , flatLayout
66 , accumPositions
67 , fileOffset
68
69 -- ** Internal
70 , sizeInBase
71
72 -- * Pieces
73 -- ** Attributes
74 , PieceIx
75 , PieceCount
76 , PieceSize
77 , minPieceSize
78 , maxPieceSize
79 , defaultPieceSize
80 , PieceHash
81
82 -- ** Piece data
83 , Piece (..)
84 , pieceSize
85 , hashPiece
86
87 -- ** Piece control
88 , HashList (..)
89 , PieceInfo (..)
90 , pieceLength
91 , pieceHashes
92 , pieceCount
93
94 -- ** Validation
95 , pieceHash
96 , checkPieceLazy
97
98 -- * Info dictionary
99 , InfoDict (..)
33 , infohash 100 , infohash
34 , layoutInfo 101 , layoutInfo
35 , pieceInfo 102 , pieceInfo
36 , isPrivate 103 , isPrivate
104 , infoDictionary
37 105
38 -- * Torrent file 106 -- * Torrent file
39 , Torrent(..) 107 , Torrent(..)
@@ -50,50 +118,598 @@ module Data.Torrent
50 , publisherURL 118 , publisherURL
51 , signature 119 , signature
52 120
53 -- * Construction 121 -- ** Utils
54 , nullTorrent 122 , nullTorrent
55
56 -- * Mime types
57 , typeTorrent 123 , typeTorrent
58
59 -- * File paths
60 , torrentExt 124 , torrentExt
61 , isTorrentPath 125 , isTorrentPath
62
63 -- * IO
64 , fromFile 126 , fromFile
65 , toFile 127 , toFile
128
129 -- * Magnet
130 -- $magnet-link
131 , Magnet(..)
132 , nullMagnet
133 , simpleMagnet
134 , detailedMagnet
135 , parseMagnet
136 , renderMagnet
137
138 -- ** URN
139 , URN (..)
140 , NamespaceId
141 , btih
142 , infohashURN
143 , parseURN
144 , renderURN
66 ) where 145 ) where
67 146
68import Prelude hiding (sum) 147import Prelude
69import Control.Applicative 148import Control.Applicative
70import qualified Crypto.Hash.SHA1 as C
71import Control.DeepSeq 149import Control.DeepSeq
72import Control.Exception 150import Control.Exception
73import Control.Lens 151import Control.Lens
152import Control.Monad
153import Crypto.Hash.SHA1 as SHA1
74import Data.BEncode as BE 154import Data.BEncode as BE
75import Data.BEncode.Types as BE 155import Data.BEncode.Types as BE
76import Data.ByteString as BS 156import Data.Bits
77import qualified Data.ByteString.Char8 as BC (pack, unpack) 157import Data.Bits.Extras
78import qualified Data.ByteString.Lazy as BL 158import Data.ByteString as BS
79import Data.Convertible 159import Data.ByteString.Base16 as Base16
80import Data.Default 160import Data.ByteString.Base32 as Base32
81import Data.Hashable as Hashable 161import Data.ByteString.Base64 as Base64
82import qualified Data.List as L 162import Data.ByteString.Char8 as BC (pack, unpack)
83import Data.Text as T 163import Data.ByteString.Lazy as BL
84import Data.Time 164import Data.Char
165import Data.Convertible
166import Data.Default
167import Data.Foldable as F
168import Data.Hashable as Hashable
169import Data.Int
170import Data.List as L
171import Data.Map as M
172import Data.Maybe
173import Data.Serialize as S
174import Data.String
175import Data.Text as T
176import Data.Text.Encoding as T
177import Data.Text.Read
85import Data.Time.Clock.POSIX 178import Data.Time.Clock.POSIX
86import Data.Typeable 179import Data.Typeable
87import Network (HostName) 180import Network (HostName)
181import Network.HTTP.Types.QueryLike
182import Network.HTTP.Types.URI
88import Network.URI 183import Network.URI
184import Text.ParserCombinators.ReadP as P
89import Text.PrettyPrint as PP 185import Text.PrettyPrint as PP
90import Text.PrettyPrint.Class 186import Text.PrettyPrint.Class
91import System.FilePath 187import System.FilePath
188import System.Posix.Types
189
190import Network.BitTorrent.Address
191
192
193{-----------------------------------------------------------------------
194-- Info hash
195-----------------------------------------------------------------------}
196-- TODO
197--
198-- data Word160 = Word160 {-# UNPACK #-} !Word64
199-- {-# UNPACK #-} !Word64
200-- {-# UNPACK #-} !Word32
201--
202-- newtype InfoHash = InfoHash Word160
203--
204-- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes
205
206-- $infohash
207--
208-- Infohash is a unique identifier of torrent.
209
210-- | Exactly 20 bytes long SHA1 hash of the info part of torrent file.
211newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
212 deriving (Eq, Ord, Typeable)
213
214infoHashLen :: Int
215infoHashLen = 20
216
217-- | Meaningless placeholder value.
218instance Default InfoHash where
219 def = "0123456789012345678901234567890123456789"
220
221-- | Hash raw bytes. (no encoding)
222instance Hashable InfoHash where
223 hashWithSalt s (InfoHash ih) = hashWithSalt s ih
224 {-# INLINE hashWithSalt #-}
225
226-- | Convert to\/from raw bencoded string. (no encoding)
227instance BEncode InfoHash where
228 toBEncode = toBEncode . getInfoHash
229 fromBEncode be = InfoHash <$> fromBEncode be
230
231-- | Convert to\/from raw bytestring. (no encoding)
232instance Serialize InfoHash where
233 put (InfoHash ih) = putByteString ih
234 {-# INLINE put #-}
235
236 get = InfoHash <$> getBytes infoHashLen
237 {-# INLINE get #-}
238
239-- | Convert to raw query value. (no encoding)
240instance QueryValueLike InfoHash where
241 toQueryValue (InfoHash ih) = Just ih
242 {-# INLINE toQueryValue #-}
243
244-- | Convert to base16 encoded string.
245instance Show InfoHash where
246 show (InfoHash ih) = BC.unpack (Base16.encode ih)
247
248-- | Convert to base16 encoded Doc string.
249instance Pretty InfoHash where
250 pretty = text . show
251
252-- | Read base16 encoded string.
253instance Read InfoHash where
254 readsPrec _ = readP_to_S $ do
255 str <- replicateM (infoHashLen * 2) (satisfy isHexDigit)
256 return $ InfoHash $ decodeIH str
257 where
258 decodeIH = BS.pack . L.map fromHex . pair
259 fromHex (a, b) = read $ '0' : 'x' : a : b : []
260
261 pair (a : b : xs) = (a, b) : pair xs
262 pair _ = []
263
264-- | Convert raw bytes to info hash.
265instance Convertible BS.ByteString InfoHash where
266 safeConvert bs
267 | BS.length bs == infoHashLen = pure (InfoHash bs)
268 | otherwise = convError "invalid length" bs
269
270-- | Parse infohash from base16\/base32\/base64 encoded string.
271instance Convertible Text InfoHash where
272 safeConvert t
273 | 20 == hashLen = pure (InfoHash hashStr)
274 | 26 <= hashLen && hashLen <= 28 =
275 case Base64.decode hashStr of
276 Left msg -> convError ("invalid base64 encoding " ++ msg) t
277 Right ihStr -> safeConvert ihStr
278
279 | hashLen == 32 =
280 case Base32.decode hashStr of
281 Left msg -> convError msg t
282 Right ihStr -> safeConvert ihStr
283
284 | hashLen == 40 =
285 let (ihStr, inv) = Base16.decode hashStr
286 in if BS.length inv /= 0
287 then convError "invalid base16 encoding" t
288 else safeConvert ihStr
289
290 | otherwise = convError "invalid length" t
291 where
292 hashLen = BS.length hashStr
293 hashStr = T.encodeUtf8 t
294
295-- | Decode from base16\/base32\/base64 encoded string.
296instance IsString InfoHash where
297 fromString = either (error . prettyConvertError) id . safeConvert . T.pack
298
299ignoreErrorMsg :: Either a b -> Maybe b
300ignoreErrorMsg = either (const Nothing) Just
301
302-- | Tries both base16 and base32 while decoding info hash.
303--
304-- Use 'safeConvert' for detailed error messages.
305--
306textToInfoHash :: Text -> Maybe InfoHash
307textToInfoHash = ignoreErrorMsg . safeConvert
92 308
93import Data.Torrent.InfoHash as IH 309-- | Hex encode infohash to text, full length.
94import Data.Torrent.Layout 310longHex :: InfoHash -> Text
95import Data.Torrent.Piece 311longHex = T.decodeUtf8 . Base16.encode . getInfoHash
96import Network.BitTorrent.Core.NodeInfo 312
313-- | The same as 'longHex', but only first 7 characters.
314shortHex :: InfoHash -> Text
315shortHex = T.take 7 . longHex
316
317{-----------------------------------------------------------------------
318-- File info
319-----------------------------------------------------------------------}
320
321-- | Size of a file in bytes.
322type FileSize = FileOffset
323
324deriving instance BEncode FileOffset
325
326-- | Contain metainfo about one single file.
327data FileInfo a = FileInfo {
328 fiLength :: {-# UNPACK #-} !FileSize
329 -- ^ Length of the file in bytes.
330
331 -- TODO unpacked MD5 sum
332 , fiMD5Sum :: !(Maybe BS.ByteString)
333 -- ^ 32 character long MD5 sum of the file. Used by third-party
334 -- tools, not by bittorrent protocol itself.
335
336 , fiName :: !a
337 -- ^ One or more string elements that together represent the
338 -- path and filename. Each element in the list corresponds to
339 -- either a directory name or (in the case of the last element)
340 -- the filename. For example, the file:
341 --
342 -- > "dir1/dir2/file.ext"
343 --
344 -- would consist of three string elements:
345 --
346 -- > ["dir1", "dir2", "file.ext"]
347 --
348 } deriving (Show, Read, Eq, Typeable
349 , Functor, Foldable
350 )
351
352makeLensesFor
353 [ ("fiLength", "fileLength")
354 , ("fiMD5Sum", "fileMD5Sum")
355 , ("fiName" , "filePath" )
356 ]
357 ''FileInfo
358
359instance NFData a => NFData (FileInfo a) where
360 rnf FileInfo {..} = rnf fiName
361 {-# INLINE rnf #-}
362
363instance BEncode (FileInfo [BS.ByteString]) where
364 toBEncode FileInfo {..} = toDict $
365 "length" .=! fiLength
366 .: "md5sum" .=? fiMD5Sum
367 .: "path" .=! fiName
368 .: endDict
369 {-# INLINE toBEncode #-}
370
371 fromBEncode = fromDict $ do
372 FileInfo <$>! "length"
373 <*>? "md5sum"
374 <*>! "path"
375 {-# INLINE fromBEncode #-}
376
377type Put a = a -> BDict -> BDict
378
379putFileInfoSingle :: Data.Torrent.Put (FileInfo BS.ByteString)
380putFileInfoSingle FileInfo {..} cont =
381 "length" .=! fiLength
382 .: "md5sum" .=? fiMD5Sum
383 .: "name" .=! fiName
384 .: cont
385
386getFileInfoSingle :: BE.Get (FileInfo BS.ByteString)
387getFileInfoSingle = do
388 FileInfo <$>! "length"
389 <*>? "md5sum"
390 <*>! "name"
391
392instance BEncode (FileInfo BS.ByteString) where
393 toBEncode = toDict . (`putFileInfoSingle` endDict)
394 {-# INLINE toBEncode #-}
395
396 fromBEncode = fromDict getFileInfoSingle
397 {-# INLINE fromBEncode #-}
398
399instance Pretty (FileInfo BS.ByteString) where
400 pretty FileInfo {..} =
401 "Path: " <> text (T.unpack (T.decodeUtf8 fiName))
402 $$ "Size: " <> text (show fiLength)
403 $$ maybe PP.empty ppMD5 fiMD5Sum
404 where
405 ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5))
406
407-- | Join file path.
408joinFilePath :: FileInfo [BS.ByteString] -> FileInfo BS.ByteString
409joinFilePath = fmap (BS.intercalate "/")
410
411{-----------------------------------------------------------------------
412-- Layout info
413-----------------------------------------------------------------------}
414
415-- | Original (found in torrent file) layout info is either:
416--
417-- * Single file with its /name/.
418--
419-- * Multiple files with its relative file /paths/.
420--
421data LayoutInfo
422 = SingleFile
423 { -- | Single file info.
424 liFile :: !(FileInfo BS.ByteString)
425 }
426 | MultiFile
427 { -- | List of the all files that torrent contains.
428 liFiles :: ![FileInfo [BS.ByteString]]
429
430 -- | The /suggested/ name of the root directory in which to
431 -- store all the files.
432 , liDirName :: !BS.ByteString
433 } deriving (Show, Read, Eq, Typeable)
434
435makeLensesFor
436 [ ("liFile" , "singleFile" )
437 , ("liFiles" , "multiFile" )
438 , ("liDirName", "rootDirName")
439 ]
440 ''LayoutInfo
441
442instance NFData LayoutInfo where
443 rnf SingleFile {..} = ()
444 rnf MultiFile {..} = rnf liFiles
445
446-- | Empty multifile layout.
447instance Default LayoutInfo where
448 def = MultiFile [] ""
449
450getLayoutInfo :: BE.Get LayoutInfo
451getLayoutInfo = single <|> multi
452 where
453 single = SingleFile <$> getFileInfoSingle
454 multi = MultiFile <$>! "files" <*>! "name"
455
456putLayoutInfo :: Data.Torrent.Put LayoutInfo
457putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
458putLayoutInfo MultiFile {..} = \ cont ->
459 "files" .=! liFiles
460 .: "name" .=! liDirName
461 .: cont
462
463instance BEncode LayoutInfo where
464 toBEncode = toDict . (`putLayoutInfo` endDict)
465 fromBEncode = fromDict getLayoutInfo
466
467instance Pretty LayoutInfo where
468 pretty SingleFile {..} = pretty liFile
469 pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles
470
471-- | Test if this is single file torrent.
472isSingleFile :: LayoutInfo -> Bool
473isSingleFile SingleFile {} = True
474isSingleFile _ = False
475{-# INLINE isSingleFile #-}
476
477-- | Test if this is multifile torrent.
478isMultiFile :: LayoutInfo -> Bool
479isMultiFile MultiFile {} = True
480isMultiFile _ = False
481{-# INLINE isMultiFile #-}
482
483-- | Get name of the torrent based on the root path piece.
484suggestedName :: LayoutInfo -> BS.ByteString
485suggestedName (SingleFile FileInfo {..}) = fiName
486suggestedName MultiFile {..} = liDirName
487{-# INLINE suggestedName #-}
488
489-- | Find sum of sizes of the all torrent files.
490contentLength :: LayoutInfo -> FileSize
491contentLength SingleFile { liFile = FileInfo {..} } = fiLength
492contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
493
494-- | Get number of all files in torrent.
495fileCount :: LayoutInfo -> Int
496fileCount SingleFile {..} = 1
497fileCount MultiFile {..} = L.length liFiles
498
499-- | Find number of blocks of the specified size. If torrent size is
500-- not a multiple of block size then the count is rounded up.
501blockCount :: Int -> LayoutInfo -> Int
502blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
503
504------------------------------------------------------------------------
505
506-- | File layout specifies the order and the size of each file in the
507-- storage. Note that order of files is highly important since we
508-- coalesce all the files in the given order to get the linear block
509-- address space.
510--
511type FileLayout a = [(FilePath, a)]
512
513-- | Extract files layout from torrent info with the given root path.
514flatLayout
515 :: FilePath -- ^ Root path for the all torrent files.
516 -> LayoutInfo -- ^ Torrent content information.
517 -> FileLayout FileSize -- ^ The all file paths prefixed with the given root.
518flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
519 = [(prefixPath </> BC.unpack fiName, fiLength)]
520flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
521 where -- TODO use utf8 encoding in name
522 mkPath FileInfo {..} = (_path, fiLength)
523 where
524 _path = prefixPath </> BC.unpack liDirName
525 </> joinPath (L.map BC.unpack fiName)
526
527-- | Calculate offset of each file based on its length, incrementally.
528accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
529accumPositions = go 0
530 where
531 go !_ [] = []
532 go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs
533
534-- | Gives global offset of a content file for a given full path.
535fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
536fileOffset = L.lookup
537{-# INLINE fileOffset #-}
538
539------------------------------------------------------------------------
540
541-- | Divide and round up.
542sizeInBase :: Integral a => a -> Int -> Int
543sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
544 where
545 align = if n `mod` fromIntegral b == 0 then 0 else 1
546{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
547{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}
548
549{-----------------------------------------------------------------------
550-- Piece attributes
551-----------------------------------------------------------------------}
552
553-- | Zero-based index of piece in torrent content.
554type PieceIx = Int
555
556-- | Size of piece in bytes. Should be a power of 2.
557--
558-- NOTE: Have max and min size constrained to wide used
559-- semi-standard values. This bounds should be used to make decision
560-- about piece size for new torrents.
561--
562type PieceSize = Int
563
564-- | Number of pieces in torrent or a part of torrent.
565type PieceCount = Int
566
567defaultBlockSize :: Int
568defaultBlockSize = 16 * 1024
569
570-- | Optimal number of pieces in torrent.
571optimalPieceCount :: PieceCount
572optimalPieceCount = 1000
573{-# INLINE optimalPieceCount #-}
574
575-- | Piece size should not be less than this value.
576minPieceSize :: Int
577minPieceSize = defaultBlockSize * 4
578{-# INLINE minPieceSize #-}
579
580-- | To prevent transfer degradation piece size should not exceed this
581-- value.
582maxPieceSize :: Int
583maxPieceSize = 4 * 1024 * 1024
584{-# INLINE maxPieceSize #-}
585
586toPow2 :: Int -> Int
587toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
588
589-- | Find the optimal piece size for a given torrent size.
590defaultPieceSize :: Int64 -> Int
591defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
592 where
593 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
594
595{-----------------------------------------------------------------------
596-- Piece data
597-----------------------------------------------------------------------}
598
599type PieceHash = BS.ByteString
600
601hashsize :: Int
602hashsize = 20
603{-# INLINE hashsize #-}
604
605-- TODO check if pieceLength is power of 2
606-- | Piece payload should be strict or lazy bytestring.
607data Piece a = Piece
608 { -- | Zero-based piece index in torrent.
609 pieceIndex :: {-# UNPACK #-} !PieceIx
610
611 -- | Payload.
612 , pieceData :: !a
613 } deriving (Show, Read, Eq, Functor, Typeable)
614
615instance NFData (Piece a)
616
617-- | Payload bytes are omitted.
618instance Pretty (Piece a) where
619 pretty Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
620
621-- | Get size of piece in bytes.
622pieceSize :: Piece BL.ByteString -> PieceSize
623pieceSize Piece {..} = fromIntegral (BL.length pieceData)
624
625-- | Get piece hash.
626hashPiece :: Piece BL.ByteString -> PieceHash
627hashPiece Piece {..} = SHA1.hashlazy pieceData
628
629{-----------------------------------------------------------------------
630-- Piece control
631-----------------------------------------------------------------------}
632
633-- | A flat array of SHA1 hash for each piece.
634newtype HashList = HashList { unHashList :: BS.ByteString }
635 deriving (Show, Read, Eq, BEncode, Typeable)
636
637-- | Empty hash list.
638instance Default HashList where
639 def = HashList ""
640
641-- | Part of torrent file used for torrent content validation.
642data PieceInfo = PieceInfo
643 { piPieceLength :: {-# UNPACK #-} !PieceSize
644 -- ^ Number of bytes in each piece.
645
646 , piPieceHashes :: !HashList
647 -- ^ Concatenation of all 20-byte SHA1 hash values.
648 } deriving (Show, Read, Eq, Typeable)
649
650-- | Number of bytes in each piece.
651makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
652
653-- | Concatenation of all 20-byte SHA1 hash values.
654makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
655
656instance NFData PieceInfo
657
658instance Default PieceInfo where
659 def = PieceInfo 1 def
660
661class Lint a where
662 lint :: a -> Either String a
663
664instance Lint PieceInfo where
665 lint pinfo @ PieceInfo {..}
666 | BS.length (unHashList piPieceHashes) `rem` hashsize == 0
667 , piPieceLength >= 0 = return pinfo
668 | otherwise = Left undefined
669
670
671putPieceInfo :: Data.Torrent.Put PieceInfo
672putPieceInfo PieceInfo {..} cont =
673 "piece length" .=! piPieceLength
674 .: "pieces" .=! piPieceHashes
675 .: cont
676
677getPieceInfo :: BE.Get PieceInfo
678getPieceInfo = do
679 PieceInfo <$>! "piece length"
680 <*>! "pieces"
681
682instance BEncode PieceInfo where
683 toBEncode = toDict . (`putPieceInfo` endDict)
684 fromBEncode = fromDict getPieceInfo
685
686-- | Hashes are omitted.
687instance Pretty PieceInfo where
688 pretty PieceInfo {..} = "Piece size: " <> int piPieceLength
689
690slice :: Int -> Int -> BS.ByteString -> BS.ByteString
691slice start len = BS.take len . BS.drop start
692{-# INLINE slice #-}
693
694-- | Extract validation hash by specified piece index.
695pieceHash :: PieceInfo -> PieceIx -> PieceHash
696pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize (unHashList piPieceHashes)
697
698-- | Find count of pieces in the torrent. If torrent size is not a
699-- multiple of piece size then the count is rounded up.
700pieceCount :: PieceInfo -> PieceCount
701pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize
702
703-- | Test if this is last piece in torrent content.
704isLastPiece :: PieceInfo -> PieceIx -> Bool
705isLastPiece ci i = pieceCount ci == succ i
706
707-- | Validate piece with metainfo hash.
708checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
709checkPieceLazy pinfo @ PieceInfo {..} Piece {..}
710 = (fromIntegral (BL.length pieceData) == piPieceLength
711 || isLastPiece pinfo pieceIndex)
712 && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex
97 713
98{----------------------------------------------------------------------- 714{-----------------------------------------------------------------------
99-- Info dictionary 715-- Info dictionary
@@ -145,9 +761,9 @@ instance Default InfoDict where
145infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict 761infoDictionary :: LayoutInfo -> PieceInfo -> Bool -> InfoDict
146infoDictionary li pinfo private = InfoDict ih li pinfo private 762infoDictionary li pinfo private = InfoDict ih li pinfo private
147 where 763 where
148 ih = hashLazyIH $ encode $ InfoDict def li pinfo private 764 ih = hashLazyIH $ BE.encode $ InfoDict def li pinfo private
149 765
150getPrivate :: Get Bool 766getPrivate :: BE.Get Bool
151getPrivate = (Just True ==) <$>? "private" 767getPrivate = (Just True ==) <$>? "private"
152 768
153putPrivate :: Bool -> BDict -> BDict 769putPrivate :: Bool -> BDict -> BDict
@@ -156,7 +772,7 @@ putPrivate True = \ cont -> "private" .=! True .: cont
156 772
157-- | Hash lazy bytestring using SHA1 algorithm. 773-- | Hash lazy bytestring using SHA1 algorithm.
158hashLazyIH :: BL.ByteString -> InfoHash 774hashLazyIH :: BL.ByteString -> InfoHash
159hashLazyIH = either (const (error msg)) id . safeConvert . C.hashlazy 775hashLazyIH = either (const (error msg)) id . safeConvert . SHA1.hashlazy
160 where 776 where
161 msg = "Infohash.hash: impossible: SHA1 is always 20 bytes long" 777 msg = "Infohash.hash: impossible: SHA1 is always 20 bytes long"
162 778
@@ -172,7 +788,7 @@ instance BEncode InfoDict where
172 <*> getPieceInfo 788 <*> getPieceInfo
173 <*> getPrivate 789 <*> getPrivate
174 where 790 where
175 ih = hashLazyIH (encode dict) 791 ih = hashLazyIH (BE.encode dict)
176 792
177ppPrivacy :: Bool -> Doc 793ppPrivacy :: Bool -> Doc
178ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public" 794ppPrivacy privacy = "Privacy: " <> if privacy then "private" else "public"
@@ -189,6 +805,7 @@ instance Pretty InfoDict where
189{----------------------------------------------------------------------- 805{-----------------------------------------------------------------------
190-- Torrent info 806-- Torrent info
191-----------------------------------------------------------------------} 807-----------------------------------------------------------------------}
808-- TODO add torrent file validation
192 809
193-- | Metainfo about particular torrent. 810-- | Metainfo about particular torrent.
194data Torrent = Torrent 811data Torrent = Torrent
@@ -219,7 +836,7 @@ data Torrent = Torrent
219 , tNodes :: !(Maybe [NodeAddr HostName]) 836 , tNodes :: !(Maybe [NodeAddr HostName])
220 -- ^ This key should be set to the /K closest/ nodes in the 837 -- ^ This key should be set to the /K closest/ nodes in the
221 -- torrent generating client's routing table. Alternatively, the 838 -- torrent generating client's routing table. Alternatively, the
222 -- key could be set to a known good 'Network.BitTorrent.Core.Node' 839 -- key could be set to a known good 'Network.BitTorrent.Address.Node'
223 -- such as one operated by the person generating the torrent. 840 -- such as one operated by the person generating the torrent.
224 -- 841 --
225 -- Please do not automatically add \"router.bittorrent.com\" to 842 -- Please do not automatically add \"router.bittorrent.com\" to
@@ -232,7 +849,7 @@ data Torrent = Torrent
232 -- authority to allow new peers onto the swarm. 849 -- authority to allow new peers onto the swarm.
233 850
234 , tPublisherURL :: !(Maybe URI) 851 , tPublisherURL :: !(Maybe URI)
235 , tSignature :: !(Maybe ByteString) 852 , tSignature :: !(Maybe BS.ByteString)
236 -- ^ The RSA signature of the info dictionary (specifically, the 853 -- ^ The RSA signature of the info dictionary (specifically, the
237 -- encrypted SHA-1 hash of the info dictionary). 854 -- encrypted SHA-1 hash of the info dictionary).
238 } deriving (Show, Eq, Typeable) 855 } deriving (Show, Eq, Typeable)
@@ -361,10 +978,314 @@ isTorrentPath filepath = takeExtension filepath == extSeparator : torrentExt
361fromFile :: FilePath -> IO Torrent 978fromFile :: FilePath -> IO Torrent
362fromFile filepath = do 979fromFile filepath = do
363 contents <- BS.readFile filepath 980 contents <- BS.readFile filepath
364 case decode contents of 981 case BE.decode contents of
365 Right !t -> return t 982 Right !t -> return t
366 Left msg -> throwIO $ userError $ msg ++ " while reading torrent file" 983 Left msg -> throwIO $ userError $ msg ++ " while reading torrent file"
367 984
368-- | Encode and write a .torrent file. 985-- | Encode and write a .torrent file.
369toFile :: FilePath -> Torrent -> IO () 986toFile :: FilePath -> Torrent -> IO ()
370toFile filepath = BL.writeFile filepath . encode 987toFile filepath = BL.writeFile filepath . BE.encode
988
989{-----------------------------------------------------------------------
990-- URN
991-----------------------------------------------------------------------}
992
993-- | Namespace identifier determines the syntactic interpretation of
994-- namespace-specific string.
995type NamespaceId = [Text]
996
997-- | BitTorrent Info Hash (hence the name) namespace
998-- identifier. Namespace-specific string /should/ be a base16\/base32
999-- encoded SHA1 hash of the corresponding torrent /info/ dictionary.
1000--
1001btih :: NamespaceId
1002btih = ["btih"]
1003
1004-- | URN is pesistent location-independent identifier for
1005-- resources. In particular, URNs are used represent torrent names
1006-- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for
1007-- more info.
1008--
1009data URN = URN
1010 { urnNamespace :: NamespaceId -- ^ a namespace identifier;
1011 , urnString :: Text -- ^ a corresponding
1012 -- namespace-specific string.
1013 } deriving (Eq, Ord, Typeable)
1014
1015-----------------------------------------------------------------------
1016
1017instance Convertible URN InfoHash where
1018 safeConvert u @ URN {..}
1019 | urnNamespace /= btih = convError "invalid namespace" u
1020 | otherwise = safeConvert urnString
1021
1022-- | Make resource name for torrent with corresponding
1023-- infohash. Infohash is base16 (hex) encoded.
1024--
1025infohashURN :: InfoHash -> URN
1026infohashURN = URN btih . longHex
1027
1028-- | Meaningless placeholder value.
1029instance Default URN where
1030 def = infohashURN def
1031
1032------------------------------------------------------------------------
1033
1034-- | Render URN to its text representation.
1035renderURN :: URN -> Text
1036renderURN URN {..}
1037 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
1038
1039instance Pretty URN where
1040 pretty = text . T.unpack . renderURN
1041
1042instance Show URN where
1043 showsPrec n = showsPrec n . T.unpack . renderURN
1044
1045instance QueryValueLike URN where
1046 toQueryValue = toQueryValue . renderURN
1047 {-# INLINE toQueryValue #-}
1048
1049-----------------------------------------------------------------------
1050
1051_unsnoc :: [a] -> Maybe ([a], a)
1052_unsnoc [] = Nothing
1053_unsnoc xs = Just (L.init xs, L.last xs)
1054
1055instance Convertible Text URN where
1056 safeConvert t = case T.split (== ':') t of
1057 uriScheme : body
1058 | T.toLower uriScheme == "urn" ->
1059 case _unsnoc body of
1060 Just (namespace, val) -> pure URN
1061 { urnNamespace = namespace
1062 , urnString = val
1063 }
1064 Nothing -> convError "missing URN string" body
1065 | otherwise -> convError "invalid URN scheme" uriScheme
1066 [] -> convError "missing URN scheme" t
1067
1068instance IsString URN where
1069 fromString = either (error . prettyConvertError) id
1070 . safeConvert . T.pack
1071
1072-- | Try to parse an URN from its text representation.
1073--
1074-- Use 'safeConvert' for detailed error messages.
1075--
1076parseURN :: Text -> Maybe URN
1077parseURN = either (const Nothing) pure . safeConvert
1078
1079{-----------------------------------------------------------------------
1080-- Magnet
1081-----------------------------------------------------------------------}
1082-- $magnet-link
1083--
1084-- Magnet URI scheme is an standard defining Magnet links. Magnet
1085-- links are refer to resources by hash, in particular magnet links
1086-- can refer to torrent using corresponding infohash. In this way,
1087-- magnet links can be used instead of torrent files.
1088--
1089-- This module provides bittorrent specific implementation of magnet
1090-- links.
1091--
1092-- For generic magnet uri scheme see:
1093-- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>,
1094-- <http://www.iana.org/assignments/uri-schemes/prov/magnet>
1095--
1096-- Bittorrent specific details:
1097-- <http://www.bittorrent.org/beps/bep_0009.html>
1098--
1099
1100-- TODO multiple exact topics
1101-- TODO render/parse supplement for URI/query
1102
1103-- | An URI used to identify torrent.
1104data Magnet = Magnet
1105 { -- | Torrent infohash hash. Can be used in DHT queries if no
1106 -- 'tracker' provided.
1107 exactTopic :: !InfoHash -- TODO InfoHash -> URN?
1108
1109 -- | A filename for the file to download. Can be used to
1110 -- display name while waiting for metadata.
1111 , displayName :: Maybe Text
1112
1113 -- | Size of the resource in bytes.
1114 , exactLength :: Maybe Integer
1115
1116 -- | URI pointing to manifest, e.g. a list of further items.
1117 , manifest :: Maybe Text
1118
1119 -- | Search string.
1120 , keywordTopic :: Maybe Text
1121
1122 -- | A source to be queried after not being able to find and
1123 -- download the file in the bittorrent network in a defined
1124 -- amount of time.
1125 , acceptableSource :: Maybe URI
1126
1127 -- | Direct link to the resource.
1128 , exactSource :: Maybe URI
1129
1130 -- | URI to the tracker.
1131 , tracker :: Maybe URI
1132
1133 -- | Additional or experimental parameters.
1134 , supplement :: Map Text Text
1135 } deriving (Eq, Ord, Typeable)
1136
1137instance QueryValueLike Integer where
1138 toQueryValue = toQueryValue . show
1139
1140instance QueryValueLike URI where
1141 toQueryValue = toQueryValue . show
1142
1143instance QueryLike Magnet where
1144 toQuery Magnet {..} =
1145 [ ("xt", toQueryValue $ infohashURN exactTopic)
1146 , ("dn", toQueryValue displayName)
1147 , ("xl", toQueryValue exactLength)
1148 , ("mt", toQueryValue manifest)
1149 , ("kt", toQueryValue keywordTopic)
1150 , ("as", toQueryValue acceptableSource)
1151 , ("xs", toQueryValue exactSource)
1152 , ("tr", toQueryValue tracker)
1153 ]
1154
1155instance QueryValueLike Magnet where
1156 toQueryValue = toQueryValue . renderMagnet
1157
1158instance Convertible QueryText Magnet where
1159 safeConvert xs = do
1160 urnStr <- getTextMsg "xt" "exact topic not defined" xs
1161 infoHash <- convertVia (error "safeConvert" :: URN) urnStr
1162 return Magnet
1163 { exactTopic = infoHash
1164 , displayName = getText "dn" xs
1165 , exactLength = getText "xl" xs >>= getInt
1166 , manifest = getText "mt" xs
1167 , keywordTopic = getText "kt" xs
1168 , acceptableSource = getText "as" xs >>= getURI
1169 , exactSource = getText "xs" xs >>= getURI
1170 , tracker = getText "tr" xs >>= getURI
1171 , supplement = M.empty
1172 }
1173 where
1174 getInt = either (const Nothing) (Just . fst) . signed decimal
1175 getURI = parseURI . T.unpack
1176 getText p = join . L.lookup p
1177 getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps
1178
1179magnetScheme :: URI
1180magnetScheme = URI
1181 { uriScheme = "magnet:"
1182 , uriAuthority = Nothing
1183 , uriPath = ""
1184 , uriQuery = ""
1185 , uriFragment = ""
1186 }
1187
1188isMagnetURI :: URI -> Bool
1189isMagnetURI u = u { uriQuery = "" } == magnetScheme
1190
1191-- | Can be used instead of 'parseMagnet'.
1192instance Convertible URI Magnet where
1193 safeConvert u @ URI {..}
1194 | not (isMagnetURI u) = convError "this is not a magnet link" u
1195 | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery
1196
1197-- | Can be used instead of 'renderMagnet'.
1198instance Convertible Magnet URI where
1199 safeConvert m = pure $ magnetScheme
1200 { uriQuery = BC.unpack $ renderQuery True $ toQuery m }
1201
1202instance Convertible String Magnet where
1203 safeConvert str
1204 | Just uri <- parseURI str = safeConvert uri
1205 | otherwise = convError "unable to parse uri" str
1206
1207------------------------------------------------------------------------
1208
1209-- | Meaningless placeholder value.
1210instance Default Magnet where
1211 def = Magnet
1212 { exactTopic = def
1213 , displayName = Nothing
1214 , exactLength = Nothing
1215 , manifest = Nothing
1216 , keywordTopic = Nothing
1217 , acceptableSource = Nothing
1218 , exactSource = Nothing
1219 , tracker = Nothing
1220 , supplement = M.empty
1221 }
1222
1223-- | Set 'exactTopic' ('xt' param) only, other params are empty.
1224nullMagnet :: InfoHash -> Magnet
1225nullMagnet u = Magnet
1226 { exactTopic = u
1227 , displayName = Nothing
1228 , exactLength = Nothing
1229 , manifest = Nothing
1230 , keywordTopic = Nothing
1231 , acceptableSource = Nothing
1232 , exactSource = Nothing
1233 , tracker = Nothing
1234 , supplement = M.empty
1235 }
1236
1237-- | Like 'nullMagnet' but also include 'displayName' ('dn' param).
1238simpleMagnet :: Torrent -> Magnet
1239simpleMagnet Torrent {tInfoDict = InfoDict {..}}
1240 = (nullMagnet idInfoHash)
1241 { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo
1242 }
1243
1244-- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and
1245-- 'tracker' ('tr' param).
1246--
1247detailedMagnet :: Torrent -> Magnet
1248detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce}
1249 = (simpleMagnet t)
1250 { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo
1251 , tracker = tAnnounce
1252 }
1253
1254-----------------------------------------------------------------------
1255
1256parseMagnetStr :: String -> Maybe Magnet
1257parseMagnetStr = either (const Nothing) Just . safeConvert
1258
1259renderMagnetStr :: Magnet -> String
1260renderMagnetStr = show . (convert :: Magnet -> URI)
1261
1262instance Pretty Magnet where
1263 pretty = PP.text . renderMagnetStr
1264
1265instance Show Magnet where
1266 show = renderMagnetStr
1267 {-# INLINE show #-}
1268
1269instance Read Magnet where
1270 readsPrec _ xs
1271 | Just m <- parseMagnetStr mstr = [(m, rest)]
1272 | otherwise = []
1273 where
1274 (mstr, rest) = L.break (== ' ') xs
1275
1276instance IsString Magnet where
1277 fromString str = fromMaybe (error msg) $ parseMagnetStr str
1278 where
1279 msg = "unable to parse magnet: " ++ str
1280
1281-- | Try to parse magnet link from urlencoded string. Use
1282-- 'safeConvert' to find out error location.
1283--
1284parseMagnet :: Text -> Maybe Magnet
1285parseMagnet = parseMagnetStr . T.unpack
1286{-# INLINE parseMagnet #-}
1287
1288-- | Render magnet link to urlencoded string
1289renderMagnet :: Magnet -> Text
1290renderMagnet = T.pack . renderMagnetStr
1291{-# INLINE renderMagnet #-}
diff --git a/src/Data/Torrent/InfoHash.hs b/src/Data/Torrent/InfoHash.hs
deleted file mode 100644
index f322ac6f..00000000
--- a/src/Data/Torrent/InfoHash.hs
+++ /dev/null
@@ -1,164 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : provisional
6-- Portability : portable
7--
8-- Infohash is a unique identifier of torrent.
9--
10{-# LANGUAGE FlexibleInstances #-}
11{-# LANGUAGE GeneralizedNewtypeDeriving #-}
12{-# LANGUAGE MultiParamTypeClasses #-}
13{-# LANGUAGE DeriveDataTypeable #-}
14module Data.Torrent.InfoHash
15 ( InfoHash
16
17 -- * Parsing
18 , textToInfoHash
19
20 -- * Rendering
21 , longHex
22 , shortHex
23 ) where
24
25import Control.Applicative
26import Control.Monad
27import Data.BEncode
28import Data.ByteString as BS
29import Data.ByteString.Char8 as BC
30import Data.ByteString.Base16 as Base16
31import Data.ByteString.Base32 as Base32
32import Data.ByteString.Base64 as Base64
33import Data.Char
34import Data.Convertible.Base
35import Data.Default
36import Data.List as L
37import Data.Hashable as Hashable
38import Data.Serialize
39import Data.String
40import Data.Text as T
41import Data.Text.Encoding as T
42import Data.Typeable
43import Network.HTTP.Types.QueryLike
44import Text.ParserCombinators.ReadP as P
45import Text.PrettyPrint
46import Text.PrettyPrint.Class
47
48
49-- TODO
50--
51-- data Word160 = Word160 {-# UNPACK #-} !Word64
52-- {-# UNPACK #-} !Word64
53-- {-# UNPACK #-} !Word32
54--
55-- newtype InfoHash = InfoHash Word160
56--
57-- reason: bytestring have overhead = 8 words, while infohash have length 20 bytes
58
59-- | Exactly 20 bytes long SHA1 hash of the info part of torrent file.
60newtype InfoHash = InfoHash { getInfoHash :: BS.ByteString }
61 deriving (Eq, Ord, Typeable)
62
63infoHashLen :: Int
64infoHashLen = 20
65
66-- | Meaningless placeholder value.
67instance Default InfoHash where
68 def = "0123456789012345678901234567890123456789"
69
70-- | Hash raw bytes. (no encoding)
71instance Hashable InfoHash where
72 hashWithSalt s (InfoHash ih) = hashWithSalt s ih
73 {-# INLINE hashWithSalt #-}
74
75-- | Convert to\/from raw bencoded string. (no encoding)
76instance BEncode InfoHash where
77 toBEncode = toBEncode . getInfoHash
78 fromBEncode be = InfoHash <$> fromBEncode be
79
80-- | Convert to\/from raw bytestring. (no encoding)
81instance Serialize InfoHash where
82 put (InfoHash ih) = putByteString ih
83 {-# INLINE put #-}
84
85 get = InfoHash <$> getBytes infoHashLen
86 {-# INLINE get #-}
87
88-- | Convert to raw query value. (no encoding)
89instance QueryValueLike InfoHash where
90 toQueryValue (InfoHash ih) = Just ih
91 {-# INLINE toQueryValue #-}
92
93-- | Convert to base16 encoded string.
94instance Show InfoHash where
95 show (InfoHash ih) = BC.unpack (Base16.encode ih)
96
97-- | Convert to base16 encoded Doc string.
98instance Pretty InfoHash where
99 pretty = text . show
100
101-- | Read base16 encoded string.
102instance Read InfoHash where
103 readsPrec _ = readP_to_S $ do
104 str <- replicateM (infoHashLen * 2) (satisfy isHexDigit)
105 return $ InfoHash $ decodeIH str
106 where
107 decodeIH = BS.pack . L.map fromHex . pair
108 fromHex (a, b) = read $ '0' : 'x' : a : b : []
109
110 pair (a : b : xs) = (a, b) : pair xs
111 pair _ = []
112
113-- | Convert raw bytes to info hash.
114instance Convertible BS.ByteString InfoHash where
115 safeConvert bs
116 | BS.length bs == infoHashLen = pure (InfoHash bs)
117 | otherwise = convError "invalid length" bs
118
119-- | Parse infohash from base16\/base32\/base64 encoded string.
120instance Convertible Text InfoHash where
121 safeConvert t
122 | 20 == hashLen = pure (InfoHash hashStr)
123 | 26 <= hashLen && hashLen <= 28 =
124 case Base64.decode hashStr of
125 Left msg -> convError ("invalid base64 encoding " ++ msg) t
126 Right ihStr -> safeConvert ihStr
127
128 | hashLen == 32 =
129 case Base32.decode hashStr of
130 Left msg -> convError msg t
131 Right ihStr -> safeConvert ihStr
132
133 | hashLen == 40 =
134 let (ihStr, inv) = Base16.decode hashStr
135 in if BS.length inv /= 0
136 then convError "invalid base16 encoding" t
137 else safeConvert ihStr
138
139 | otherwise = convError "invalid length" t
140 where
141 hashLen = BS.length hashStr
142 hashStr = T.encodeUtf8 t
143
144-- | Decode from base16\/base32\/base64 encoded string.
145instance IsString InfoHash where
146 fromString = either (error . prettyConvertError) id . safeConvert . T.pack
147
148ignoreErrorMsg :: Either a b -> Maybe b
149ignoreErrorMsg = either (const Nothing) Just
150
151-- | Tries both base16 and base32 while decoding info hash.
152--
153-- Use 'safeConvert' for detailed error messages.
154--
155textToInfoHash :: Text -> Maybe InfoHash
156textToInfoHash = ignoreErrorMsg . safeConvert
157
158-- | Hex encode infohash to text, full length.
159longHex :: InfoHash -> Text
160longHex = T.decodeUtf8 . Base16.encode . getInfoHash
161
162-- | The same as 'longHex', but only first 7 characters.
163shortHex :: InfoHash -> Text
164shortHex = T.take 7 . longHex
diff --git a/src/Data/Torrent/Layout.hs b/src/Data/Torrent/Layout.hs
deleted file mode 100644
index cc529840..00000000
--- a/src/Data/Torrent/Layout.hs
+++ /dev/null
@@ -1,321 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Layout of files in torrent.
9--
10{-# LANGUAGE BangPatterns #-}
11{-# LANGUAGE FlexibleInstances #-}
12{-# LANGUAGE StandaloneDeriving #-}
13{-# LANGUAGE GeneralizedNewtypeDeriving #-}
14{-# LANGUAGE DeriveDataTypeable #-}
15{-# LANGUAGE DeriveFunctor #-}
16{-# LANGUAGE DeriveFoldable #-}
17{-# LANGUAGE DeriveTraversable #-}
18{-# LANGUAGE TemplateHaskell #-}
19{-# OPTIONS -fno-warn-orphans #-}
20module Data.Torrent.Layout
21 ( -- * File attributes
22 FileOffset
23 , FileSize
24
25 -- * Single file info
26 , FileInfo (..)
27
28 -- ** Lens
29 , fileLength
30 , filePath
31 , fileMD5Sum
32
33 -- * File layout
34 , LayoutInfo (..)
35 , joinFilePath
36
37 -- ** Lens
38 , singleFile
39 , multiFile
40 , rootDirName
41
42 -- ** Predicates
43 , isSingleFile
44 , isMultiFile
45
46 -- ** Query
47 , suggestedName
48 , contentLength
49 , fileCount
50 , blockCount
51
52 -- * Flat file layout
53 , FileLayout
54 , flatLayout
55 , accumPositions
56 , fileOffset
57
58 -- * Internal
59 , sizeInBase
60 , getLayoutInfo
61 , putLayoutInfo
62 ) where
63
64import Control.Applicative
65import Control.DeepSeq
66import Control.Lens
67import Data.BEncode
68import Data.BEncode.Types
69import Data.ByteString as BS
70import Data.ByteString.Base16 as Base16
71import Data.ByteString.Char8 as BC
72import Data.Default
73import Data.Foldable as F
74import Data.List as L
75import Data.Text as T
76import Data.Text.Encoding as T
77import Data.Typeable
78import Text.PrettyPrint as PP
79import Text.PrettyPrint.Class
80import System.FilePath
81import System.Posix.Types
82
83{-----------------------------------------------------------------------
84-- File attribytes
85-----------------------------------------------------------------------}
86
87-- | Size of a file in bytes.
88type FileSize = FileOffset
89
90deriving instance BEncode FileOffset
91
92{-----------------------------------------------------------------------
93-- File info both either from info dict or file list
94-----------------------------------------------------------------------}
95
96-- | Contain metainfo about one single file.
97data FileInfo a = FileInfo {
98 fiLength :: {-# UNPACK #-} !FileSize
99 -- ^ Length of the file in bytes.
100
101 -- TODO unpacked MD5 sum
102 , fiMD5Sum :: !(Maybe ByteString)
103 -- ^ 32 character long MD5 sum of the file. Used by third-party
104 -- tools, not by bittorrent protocol itself.
105
106 , fiName :: !a
107 -- ^ One or more string elements that together represent the
108 -- path and filename. Each element in the list corresponds to
109 -- either a directory name or (in the case of the last element)
110 -- the filename. For example, the file:
111 --
112 -- > "dir1/dir2/file.ext"
113 --
114 -- would consist of three string elements:
115 --
116 -- > ["dir1", "dir2", "file.ext"]
117 --
118 } deriving (Show, Read, Eq, Typeable
119 , Functor, Foldable
120 )
121
122makeLensesFor
123 [ ("fiLength", "fileLength")
124 , ("fiMD5Sum", "fileMD5Sum")
125 , ("fiName" , "filePath" )
126 ]
127 ''FileInfo
128
129instance NFData a => NFData (FileInfo a) where
130 rnf FileInfo {..} = rnf fiName
131 {-# INLINE rnf #-}
132
133instance BEncode (FileInfo [ByteString]) where
134 toBEncode FileInfo {..} = toDict $
135 "length" .=! fiLength
136 .: "md5sum" .=? fiMD5Sum
137 .: "path" .=! fiName
138 .: endDict
139 {-# INLINE toBEncode #-}
140
141 fromBEncode = fromDict $ do
142 FileInfo <$>! "length"
143 <*>? "md5sum"
144 <*>! "path"
145 {-# INLINE fromBEncode #-}
146
147type Put a = a -> BDict -> BDict
148
149putFileInfoSingle :: Put (FileInfo ByteString)
150putFileInfoSingle FileInfo {..} cont =
151 "length" .=! fiLength
152 .: "md5sum" .=? fiMD5Sum
153 .: "name" .=! fiName
154 .: cont
155
156getFileInfoSingle :: Get (FileInfo ByteString)
157getFileInfoSingle = do
158 FileInfo <$>! "length"
159 <*>? "md5sum"
160 <*>! "name"
161
162instance BEncode (FileInfo ByteString) where
163 toBEncode = toDict . (`putFileInfoSingle` endDict)
164 {-# INLINE toBEncode #-}
165
166 fromBEncode = fromDict getFileInfoSingle
167 {-# INLINE fromBEncode #-}
168
169instance Pretty (FileInfo BS.ByteString) where
170 pretty FileInfo {..} =
171 "Path: " <> text (T.unpack (T.decodeUtf8 fiName))
172 $$ "Size: " <> text (show fiLength)
173 $$ maybe PP.empty ppMD5 fiMD5Sum
174 where
175 ppMD5 md5 = "MD5 : " <> text (show (Base16.encode md5))
176
177-- | Join file path.
178joinFilePath :: FileInfo [ByteString] -> FileInfo ByteString
179joinFilePath = fmap (BS.intercalate "/")
180
181{-----------------------------------------------------------------------
182-- Original torrent file layout info
183-----------------------------------------------------------------------}
184
185-- | Original (found in torrent file) layout info is either:
186--
187-- * Single file with its /name/.
188--
189-- * Multiple files with its relative file /paths/.
190--
191data LayoutInfo
192 = SingleFile
193 { -- | Single file info.
194 liFile :: !(FileInfo ByteString)
195 }
196 | MultiFile
197 { -- | List of the all files that torrent contains.
198 liFiles :: ![FileInfo [ByteString]]
199
200 -- | The /suggested/ name of the root directory in which to
201 -- store all the files.
202 , liDirName :: !ByteString
203 } deriving (Show, Read, Eq, Typeable)
204
205makeLensesFor
206 [ ("liFile" , "singleFile" )
207 , ("liFiles" , "multiFile" )
208 , ("liDirName", "rootDirName")
209 ]
210 ''LayoutInfo
211
212instance NFData LayoutInfo where
213 rnf SingleFile {..} = ()
214 rnf MultiFile {..} = rnf liFiles
215
216-- | Empty multifile layout.
217instance Default LayoutInfo where
218 def = MultiFile [] ""
219
220getLayoutInfo :: Get LayoutInfo
221getLayoutInfo = single <|> multi
222 where
223 single = SingleFile <$> getFileInfoSingle
224 multi = MultiFile <$>! "files" <*>! "name"
225
226putLayoutInfo :: Put LayoutInfo
227putLayoutInfo SingleFile {..} = putFileInfoSingle liFile
228putLayoutInfo MultiFile {..} = \ cont ->
229 "files" .=! liFiles
230 .: "name" .=! liDirName
231 .: cont
232
233instance BEncode LayoutInfo where
234 toBEncode = toDict . (`putLayoutInfo` endDict)
235 fromBEncode = fromDict getLayoutInfo
236
237instance Pretty LayoutInfo where
238 pretty SingleFile {..} = pretty liFile
239 pretty MultiFile {..} = vcat $ L.map (pretty . joinFilePath) liFiles
240
241-- | Test if this is single file torrent.
242isSingleFile :: LayoutInfo -> Bool
243isSingleFile SingleFile {} = True
244isSingleFile _ = False
245{-# INLINE isSingleFile #-}
246
247-- | Test if this is multifile torrent.
248isMultiFile :: LayoutInfo -> Bool
249isMultiFile MultiFile {} = True
250isMultiFile _ = False
251{-# INLINE isMultiFile #-}
252
253-- | Get name of the torrent based on the root path piece.
254suggestedName :: LayoutInfo -> ByteString
255suggestedName (SingleFile FileInfo {..}) = fiName
256suggestedName MultiFile {..} = liDirName
257{-# INLINE suggestedName #-}
258
259-- | Find sum of sizes of the all torrent files.
260contentLength :: LayoutInfo -> FileSize
261contentLength SingleFile { liFile = FileInfo {..} } = fiLength
262contentLength MultiFile { liFiles = tfs } = L.sum (L.map fiLength tfs)
263
264-- | Get number of all files in torrent.
265fileCount :: LayoutInfo -> Int
266fileCount SingleFile {..} = 1
267fileCount MultiFile {..} = L.length liFiles
268
269-- | Find number of blocks of the specified size. If torrent size is
270-- not a multiple of block size then the count is rounded up.
271blockCount :: Int -> LayoutInfo -> Int
272blockCount blkSize ci = contentLength ci `sizeInBase` blkSize
273
274{-----------------------------------------------------------------------
275-- Flat layout
276-----------------------------------------------------------------------}
277
278-- | File layout specifies the order and the size of each file in the
279-- storage. Note that order of files is highly important since we
280-- coalesce all the files in the given order to get the linear block
281-- address space.
282--
283type FileLayout a = [(FilePath, a)]
284
285-- | Extract files layout from torrent info with the given root path.
286flatLayout
287 :: FilePath -- ^ Root path for the all torrent files.
288 -> LayoutInfo -- ^ Torrent content information.
289 -> FileLayout FileSize -- ^ The all file paths prefixed with the given root.
290flatLayout prefixPath SingleFile { liFile = FileInfo {..} }
291 = [(prefixPath </> BC.unpack fiName, fiLength)]
292flatLayout prefixPath MultiFile {..} = L.map mkPath liFiles
293 where -- TODO use utf8 encoding in name
294 mkPath FileInfo {..} = (path, fiLength)
295 where
296 path = prefixPath </> BC.unpack liDirName
297 </> joinPath (L.map BC.unpack fiName)
298
299-- | Calculate offset of each file based on its length, incrementally.
300accumPositions :: FileLayout FileSize -> FileLayout (FileOffset, FileSize)
301accumPositions = go 0
302 where
303 go !_ [] = []
304 go !offset ((n, s) : xs) = (n, (offset, s)) : go (offset + s) xs
305
306-- | Gives global offset of a content file for a given full path.
307fileOffset :: FilePath -> FileLayout FileOffset -> Maybe FileOffset
308fileOffset = lookup
309{-# INLINE fileOffset #-}
310
311{-----------------------------------------------------------------------
312-- Internal utilities
313-----------------------------------------------------------------------}
314
315-- | Divide and round up.
316sizeInBase :: Integral a => a -> Int -> Int
317sizeInBase n b = fromIntegral (n `div` fromIntegral b) + align
318 where
319 align = if n `mod` fromIntegral b == 0 then 0 else 1
320{-# SPECIALIZE sizeInBase :: Int -> Int -> Int #-}
321{-# SPECIALIZE sizeInBase :: Integer -> Int -> Int #-}
diff --git a/src/Data/Torrent/Magnet.hs b/src/Data/Torrent/Magnet.hs
deleted file mode 100644
index aad0debe..00000000
--- a/src/Data/Torrent/Magnet.hs
+++ /dev/null
@@ -1,372 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : provisional
6-- Portability : portable
7--
8-- Magnet URI scheme is an standard defining Magnet links. Magnet
9-- links are refer to resources by hash, in particular magnet links
10-- can refer to torrent using corresponding infohash. In this way,
11-- magnet links can be used instead of torrent files.
12--
13-- This module provides bittorrent specific implementation of magnet
14-- links.
15--
16-- For generic magnet uri scheme see:
17-- <http://magnet-uri.sourceforge.net/magnet-draft-overview.txt>,
18-- <http://www.iana.org/assignments/uri-schemes/prov/magnet>
19--
20-- Bittorrent specific details:
21-- <http://www.bittorrent.org/beps/bep_0009.html>
22--
23{-# LANGUAGE NamedFieldPuns #-}
24{-# LANGUAGE FlexibleInstances #-}
25{-# LANGUAGE MultiParamTypeClasses #-}
26{-# LANGUAGE TypeSynonymInstances #-}
27{-# LANGUAGE DeriveDataTypeable #-}
28{-# OPTIONS -fno-warn-orphans #-}
29module Data.Torrent.Magnet
30 ( -- * Magnet
31 Magnet(..)
32
33 -- ** Construction
34 , nullMagnet
35 , simpleMagnet
36 , detailedMagnet
37
38 -- ** Conversion
39 , parseMagnet
40 , renderMagnet
41
42 -- * URN
43 , URN (..)
44
45 -- ** Namespaces
46 , NamespaceId
47 , btih
48
49 -- ** Construction
50 , infohashURN
51
52 -- ** Conversion
53 , parseURN
54 , renderURN
55 ) where
56
57import Control.Applicative
58import Control.Monad
59import Data.ByteString.Char8 as BC
60import Data.Convertible
61import Data.Default
62import Data.Map as M
63import Data.Maybe
64import Data.List as L
65import Data.String
66import Data.Text as T
67import Data.Text.Encoding as T
68import Data.Text.Read
69import Data.Typeable
70import Network.HTTP.Types.QueryLike
71import Network.HTTP.Types.URI
72import Network.URI
73import Text.PrettyPrint as PP
74import Text.PrettyPrint.Class
75
76import Data.Torrent
77import Data.Torrent.InfoHash
78import Data.Torrent.Layout
79
80
81-- | Namespace identifier determines the syntactic interpretation of
82-- namespace-specific string.
83type NamespaceId = [Text]
84
85-- | BitTorrent Info Hash (hence the name) namespace
86-- identifier. Namespace-specific string /should/ be a base16\/base32
87-- encoded SHA1 hash of the corresponding torrent /info/ dictionary.
88--
89btih :: NamespaceId
90btih = ["btih"]
91
92-- | URN is pesistent location-independent identifier for
93-- resources. In particular, URNs are used represent torrent names
94-- as a part of magnet link, see 'Data.Torrent.Magnet.Magnet' for
95-- more info.
96--
97data URN = URN
98 { urnNamespace :: NamespaceId -- ^ a namespace identifier;
99 , urnString :: Text -- ^ a corresponding
100 -- namespace-specific string.
101 } deriving (Eq, Ord, Typeable)
102
103{-----------------------------------------------------------------------
104-- URN to infohash convertion
105-----------------------------------------------------------------------}
106
107instance Convertible URN InfoHash where
108 safeConvert u @ URN {..}
109 | urnNamespace /= btih = convError "invalid namespace" u
110 | otherwise = safeConvert urnString
111
112-- | Make resource name for torrent with corresponding
113-- infohash. Infohash is base16 (hex) encoded.
114--
115infohashURN :: InfoHash -> URN
116infohashURN = URN btih . longHex
117
118-- | Meaningless placeholder value.
119instance Default URN where
120 def = infohashURN def
121
122{-----------------------------------------------------------------------
123-- URN Rendering
124-----------------------------------------------------------------------}
125
126-- | Render URN to its text representation.
127renderURN :: URN -> Text
128renderURN URN {..}
129 = T.intercalate ":" $ "urn" : urnNamespace ++ [urnString]
130
131instance Pretty URN where
132 pretty = text . T.unpack . renderURN
133
134instance Show URN where
135 showsPrec n = showsPrec n . T.unpack . renderURN
136
137instance QueryValueLike URN where
138 toQueryValue = toQueryValue . renderURN
139 {-# INLINE toQueryValue #-}
140
141{-----------------------------------------------------------------------
142-- URN Parsing
143-----------------------------------------------------------------------}
144
145unsnoc :: [a] -> Maybe ([a], a)
146unsnoc [] = Nothing
147unsnoc xs = Just (L.init xs, L.last xs)
148
149instance Convertible Text URN where
150 safeConvert t = case T.split (== ':') t of
151 uriScheme : body
152 | T.toLower uriScheme == "urn" ->
153 case unsnoc body of
154 Just (namespace, val) -> pure URN
155 { urnNamespace = namespace
156 , urnString = val
157 }
158 Nothing -> convError "missing URN string" body
159 | otherwise -> convError "invalid URN scheme" uriScheme
160 [] -> convError "missing URN scheme" t
161
162instance IsString URN where
163 fromString = either (error . prettyConvertError) id
164 . safeConvert . T.pack
165
166-- | Try to parse an URN from its text representation.
167--
168-- Use 'safeConvert' for detailed error messages.
169--
170parseURN :: Text -> Maybe URN
171parseURN = either (const Nothing) pure . safeConvert
172
173{-----------------------------------------------------------------------
174-- Magnet
175-----------------------------------------------------------------------}
176
177-- TODO multiple exact topics
178-- TODO render/parse supplement for URI/query
179
180-- | An URI used to identify torrent.
181data Magnet = Magnet
182 { -- | Torrent infohash hash. Can be used in DHT queries if no
183 -- 'tracker' provided.
184 exactTopic :: !InfoHash -- TODO InfoHash -> URN?
185
186 -- | A filename for the file to download. Can be used to
187 -- display name while waiting for metadata.
188 , displayName :: Maybe Text
189
190 -- | Size of the resource in bytes.
191 , exactLength :: Maybe Integer
192
193 -- | URI pointing to manifest, e.g. a list of further items.
194 , manifest :: Maybe Text
195
196 -- | Search string.
197 , keywordTopic :: Maybe Text
198
199 -- | A source to be queried after not being able to find and
200 -- download the file in the bittorrent network in a defined
201 -- amount of time.
202 , acceptableSource :: Maybe URI
203
204 -- | Direct link to the resource.
205 , exactSource :: Maybe URI
206
207 -- | URI to the tracker.
208 , tracker :: Maybe URI
209
210 -- | Additional or experimental parameters.
211 , supplement :: Map Text Text
212 } deriving (Eq, Ord, Typeable)
213
214instance QueryValueLike Integer where
215 toQueryValue = toQueryValue . show
216
217instance QueryValueLike URI where
218 toQueryValue = toQueryValue . show
219
220instance QueryLike Magnet where
221 toQuery Magnet {..} =
222 [ ("xt", toQueryValue $ infohashURN exactTopic)
223 , ("dn", toQueryValue displayName)
224 , ("xl", toQueryValue exactLength)
225 , ("mt", toQueryValue manifest)
226 , ("kt", toQueryValue keywordTopic)
227 , ("as", toQueryValue acceptableSource)
228 , ("xs", toQueryValue exactSource)
229 , ("tr", toQueryValue tracker)
230 ]
231
232instance QueryValueLike Magnet where
233 toQueryValue = toQueryValue . renderMagnet
234
235instance Convertible QueryText Magnet where
236 safeConvert xs = do
237 urnStr <- getTextMsg "xt" "exact topic not defined" xs
238 infoHash <- convertVia (error "safeConvert" :: URN) urnStr
239 return Magnet
240 { exactTopic = infoHash
241 , displayName = getText "dn" xs
242 , exactLength = getText "xl" xs >>= getInt
243 , manifest = getText "mt" xs
244 , keywordTopic = getText "kt" xs
245 , acceptableSource = getText "as" xs >>= getURI
246 , exactSource = getText "xs" xs >>= getURI
247 , tracker = getText "tr" xs >>= getURI
248 , supplement = M.empty
249 }
250 where
251 getInt = either (const Nothing) (Just . fst) . signed decimal
252 getURI = parseURI . T.unpack
253 getText p = join . L.lookup p
254 getTextMsg p msg ps = maybe (convError msg xs) pure $ getText p ps
255
256magnetScheme :: URI
257magnetScheme = URI
258 { uriScheme = "magnet:"
259 , uriAuthority = Nothing
260 , uriPath = ""
261 , uriQuery = ""
262 , uriFragment = ""
263 }
264
265isMagnetURI :: URI -> Bool
266isMagnetURI u = u { uriQuery = "" } == magnetScheme
267
268-- | Can be used instead of 'parseMagnet'.
269instance Convertible URI Magnet where
270 safeConvert u @ URI {..}
271 | not (isMagnetURI u) = convError "this is not a magnet link" u
272 | otherwise = safeConvert $ parseQueryText $ BC.pack uriQuery
273
274-- | Can be used instead of 'renderMagnet'.
275instance Convertible Magnet URI where
276 safeConvert m = pure $ magnetScheme
277 { uriQuery = BC.unpack $ renderQuery True $ toQuery m }
278
279instance Convertible String Magnet where
280 safeConvert str
281 | Just uri <- parseURI str = safeConvert uri
282 | otherwise = convError "unable to parse uri" str
283
284{-----------------------------------------------------------------------
285-- Magnet Construction
286-----------------------------------------------------------------------}
287
288-- | Meaningless placeholder value.
289instance Default Magnet where
290 def = Magnet
291 { exactTopic = def
292 , displayName = Nothing
293 , exactLength = Nothing
294 , manifest = Nothing
295 , keywordTopic = Nothing
296 , acceptableSource = Nothing
297 , exactSource = Nothing
298 , tracker = Nothing
299 , supplement = M.empty
300 }
301
302-- | Set 'exactTopic' ('xt' param) only, other params are empty.
303nullMagnet :: InfoHash -> Magnet
304nullMagnet u = Magnet
305 { exactTopic = u
306 , displayName = Nothing
307 , exactLength = Nothing
308 , manifest = Nothing
309 , keywordTopic = Nothing
310 , acceptableSource = Nothing
311 , exactSource = Nothing
312 , tracker = Nothing
313 , supplement = M.empty
314 }
315
316-- | Like 'nullMagnet' but also include 'displayName' ('dn' param).
317simpleMagnet :: Torrent -> Magnet
318simpleMagnet Torrent {tInfoDict = InfoDict {..}}
319 = (nullMagnet idInfoHash)
320 { displayName = Just $ T.decodeUtf8 $ suggestedName idLayoutInfo
321 }
322
323-- | Like 'simpleMagnet' but also include 'exactLength' ('xl' param) and
324-- 'tracker' ('tr' param).
325--
326detailedMagnet :: Torrent -> Magnet
327detailedMagnet t @ Torrent {tInfoDict = InfoDict {..}, tAnnounce}
328 = (simpleMagnet t)
329 { exactLength = Just $ fromIntegral $ contentLength idLayoutInfo
330 , tracker = tAnnounce
331 }
332
333{-----------------------------------------------------------------------
334-- Magnet Conversion
335-----------------------------------------------------------------------}
336
337parseMagnetStr :: String -> Maybe Magnet
338parseMagnetStr = either (const Nothing) Just . safeConvert
339
340renderMagnetStr :: Magnet -> String
341renderMagnetStr = show . (convert :: Magnet -> URI)
342
343instance Pretty Magnet where
344 pretty = PP.text . renderMagnetStr
345
346instance Show Magnet where
347 show = renderMagnetStr
348 {-# INLINE show #-}
349
350instance Read Magnet where
351 readsPrec _ xs
352 | Just m <- parseMagnetStr mstr = [(m, rest)]
353 | otherwise = []
354 where
355 (mstr, rest) = L.break (== ' ') xs
356
357instance IsString Magnet where
358 fromString str = fromMaybe (error msg) $ parseMagnetStr str
359 where
360 msg = "unable to parse magnet: " ++ str
361
362-- | Try to parse magnet link from urlencoded string. Use
363-- 'safeConvert' to find out error location.
364--
365parseMagnet :: Text -> Maybe Magnet
366parseMagnet = parseMagnetStr . T.unpack
367{-# INLINE parseMagnet #-}
368
369-- | Render magnet link to urlencoded string
370renderMagnet :: Magnet -> Text
371renderMagnet = T.pack . renderMagnetStr
372{-# INLINE renderMagnet #-}
diff --git a/src/Data/Torrent/Piece.hs b/src/Data/Torrent/Piece.hs
deleted file mode 100644
index d4b2c399..00000000
--- a/src/Data/Torrent/Piece.hs
+++ /dev/null
@@ -1,232 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Pieces are used to validate torrent content.
9--
10{-# LANGUAGE TemplateHaskell #-}
11{-# LANGUAGE DeriveDataTypeable #-}
12{-# LANGUAGE DeriveFunctor #-}
13{-# LANGUAGE GeneralizedNewtypeDeriving #-}
14module Data.Torrent.Piece
15 ( -- * Piece attributes
16 PieceIx
17 , PieceCount
18 , PieceSize
19 , minPieceSize
20 , maxPieceSize
21 , defaultPieceSize
22 , PieceHash
23
24 -- * Piece data
25 , Piece (..)
26 , pieceSize
27 , hashPiece
28
29 -- * Piece control
30 , HashList (..)
31 , PieceInfo (..)
32 , pieceCount
33
34 -- * Lens
35 , pieceLength
36 , pieceHashes
37
38 -- * Validation
39 , pieceHash
40 , checkPieceLazy
41
42 -- * Internal
43 , getPieceInfo
44 , putPieceInfo
45 ) where
46
47import Control.DeepSeq
48import Control.Lens
49import qualified Crypto.Hash.SHA1 as SHA1
50import Data.BEncode
51import Data.BEncode.Types
52import Data.Bits
53import Data.Bits.Extras
54import Data.ByteString as BS
55import qualified Data.ByteString.Lazy as BL
56import qualified Data.ByteString.Base64 as Base64
57import Data.Default
58import Data.Int
59import Data.Text.Encoding as T
60import Data.Typeable
61import Text.PrettyPrint
62import Text.PrettyPrint.Class
63
64
65-- TODO add torrent file validation
66class Lint a where
67 lint :: a -> Either String a
68
69--class Validation a where
70-- validate :: PieceInfo -> Piece a -> Bool
71
72{-----------------------------------------------------------------------
73-- Piece attributes
74-----------------------------------------------------------------------}
75
76-- | Zero-based index of piece in torrent content.
77type PieceIx = Int
78
79-- | Size of piece in bytes. Should be a power of 2.
80--
81-- NOTE: Have max and min size constrained to wide used
82-- semi-standard values. This bounds should be used to make decision
83-- about piece size for new torrents.
84--
85type PieceSize = Int
86
87-- | Number of pieces in torrent or a part of torrent.
88type PieceCount = Int
89
90defaultBlockSize :: Int
91defaultBlockSize = 16 * 1024
92
93-- | Optimal number of pieces in torrent.
94optimalPieceCount :: PieceCount
95optimalPieceCount = 1000
96{-# INLINE optimalPieceCount #-}
97
98-- | Piece size should not be less than this value.
99minPieceSize :: Int
100minPieceSize = defaultBlockSize * 4
101{-# INLINE minPieceSize #-}
102
103-- | To prevent transfer degradation piece size should not exceed this
104-- value.
105maxPieceSize :: Int
106maxPieceSize = 4 * 1024 * 1024
107{-# INLINE maxPieceSize #-}
108
109toPow2 :: Int -> Int
110toPow2 x = bit $ fromIntegral (leadingZeros (0 :: Int) - leadingZeros x)
111
112-- | Find the optimal piece size for a given torrent size.
113defaultPieceSize :: Int64 -> Int
114defaultPieceSize x = max minPieceSize $ min maxPieceSize $ toPow2 pc
115 where
116 pc = fromIntegral (x `div` fromIntegral optimalPieceCount)
117
118{-----------------------------------------------------------------------
119-- Piece data
120-----------------------------------------------------------------------}
121
122type PieceHash = ByteString
123
124hashsize :: Int
125hashsize = 20
126{-# INLINE hashsize #-}
127
128-- TODO check if pieceLength is power of 2
129-- | Piece payload should be strict or lazy bytestring.
130data Piece a = Piece
131 { -- | Zero-based piece index in torrent.
132 pieceIndex :: {-# UNPACK #-} !PieceIx
133
134 -- | Payload.
135 , pieceData :: !a
136 } deriving (Show, Read, Eq, Functor, Typeable)
137
138instance NFData (Piece a)
139
140-- | Payload bytes are omitted.
141instance Pretty (Piece a) where
142 pretty Piece {..} = "Piece" <+> braces ("index" <+> "=" <+> int pieceIndex)
143
144-- | Get size of piece in bytes.
145pieceSize :: Piece BL.ByteString -> PieceSize
146pieceSize Piece {..} = fromIntegral (BL.length pieceData)
147
148-- | Get piece hash.
149hashPiece :: Piece BL.ByteString -> PieceHash
150hashPiece Piece {..} = SHA1.hashlazy pieceData
151
152{-----------------------------------------------------------------------
153-- Piece control
154-----------------------------------------------------------------------}
155
156-- | A flat array of SHA1 hash for each piece.
157newtype HashList = HashList { unHashList :: ByteString }
158 deriving (Show, Read, Eq, BEncode, Typeable)
159
160-- | Empty hash list.
161instance Default HashList where
162 def = HashList ""
163
164-- | Part of torrent file used for torrent content validation.
165data PieceInfo = PieceInfo
166 { piPieceLength :: {-# UNPACK #-} !PieceSize
167 -- ^ Number of bytes in each piece.
168
169 , piPieceHashes :: !HashList
170 -- ^ Concatenation of all 20-byte SHA1 hash values.
171 } deriving (Show, Read, Eq, Typeable)
172
173-- | Number of bytes in each piece.
174makeLensesFor [("piPieceLength", "pieceLength")] ''PieceInfo
175
176-- | Concatenation of all 20-byte SHA1 hash values.
177makeLensesFor [("piPieceHashes", "pieceHashes")] ''PieceInfo
178
179instance NFData PieceInfo
180
181instance Default PieceInfo where
182 def = PieceInfo 1 def
183
184instance Lint PieceInfo where
185 lint pinfo @ PieceInfo {..}
186 | BS.length (unHashList piPieceHashes) `rem` hashsize == 0
187 , piPieceLength >= 0 = return pinfo
188 | otherwise = Left undefined
189
190
191putPieceInfo :: PieceInfo -> BDict -> BDict
192putPieceInfo PieceInfo {..} cont =
193 "piece length" .=! piPieceLength
194 .: "pieces" .=! piPieceHashes
195 .: cont
196
197getPieceInfo :: Get PieceInfo
198getPieceInfo = do
199 PieceInfo <$>! "piece length"
200 <*>! "pieces"
201
202instance BEncode PieceInfo where
203 toBEncode = toDict . (`putPieceInfo` endDict)
204 fromBEncode = fromDict getPieceInfo
205
206-- | Hashes are omitted.
207instance Pretty PieceInfo where
208 pretty PieceInfo {..} = "Piece size: " <> int piPieceLength
209
210slice :: Int -> Int -> ByteString -> ByteString
211slice start len = BS.take len . BS.drop start
212{-# INLINE slice #-}
213
214-- | Extract validation hash by specified piece index.
215pieceHash :: PieceInfo -> PieceIx -> PieceHash
216pieceHash PieceInfo {..} i = slice (hashsize * i) hashsize (unHashList piPieceHashes)
217
218-- | Find count of pieces in the torrent. If torrent size is not a
219-- multiple of piece size then the count is rounded up.
220pieceCount :: PieceInfo -> PieceCount
221pieceCount PieceInfo {..} = BS.length (unHashList piPieceHashes) `quot` hashsize
222
223-- | Test if this is last piece in torrent content.
224isLastPiece :: PieceInfo -> PieceIx -> Bool
225isLastPiece ci i = pieceCount ci == succ i
226
227-- | Validate piece with metainfo hash.
228checkPieceLazy :: PieceInfo -> Piece BL.ByteString -> Bool
229checkPieceLazy pinfo @ PieceInfo {..} Piece {..}
230 = (fromIntegral (BL.length pieceData) == piPieceLength
231 || isLastPiece pinfo pieceIndex)
232 && SHA1.hashlazy pieceData == pieceHash pinfo pieceIndex
diff --git a/src/Network/BitTorrent.hs b/src/Network/BitTorrent.hs
index bcc7cfcf..91a58887 100644
--- a/src/Network/BitTorrent.hs
+++ b/src/Network/BitTorrent.hs
@@ -57,7 +57,5 @@ module Network.BitTorrent
57 ) where 57 ) where
58 58
59import Data.Torrent 59import Data.Torrent
60import Data.Torrent.InfoHash
61import Data.Torrent.Magnet
62import Network.BitTorrent.Client 60import Network.BitTorrent.Client
63import Network.BitTorrent.Internal.Types \ No newline at end of file 61import Network.BitTorrent.Internal.Types \ No newline at end of file
diff --git a/src/Network/BitTorrent/Address.hs b/src/Network/BitTorrent/Address.hs
new file mode 100644
index 00000000..eeb04c74
--- /dev/null
+++ b/src/Network/BitTorrent/Address.hs
@@ -0,0 +1,1172 @@
1-- |
2-- Module : Network.BitTorrent.Address
3-- Copyright : (c) Sam Truzjan 2013
4-- (c) Daniel Gröber 2013
5-- License : BSD3
6-- Maintainer : pxqr.sta@gmail.com
7-- Stability : provisional
8-- Portability : portable
9--
10-- Peer and Node addresses.
11--
12{-# LANGUAGE FlexibleInstances #-}
13{-# LANGUAGE RecordWildCards #-}
14{-# LANGUAGE StandaloneDeriving #-}
15{-# LANGUAGE ViewPatterns #-}
16{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17{-# LANGUAGE MultiParamTypeClasses #-}
18{-# LANGUAGE DeriveDataTypeable #-}
19{-# LANGUAGE DeriveFunctor #-}
20{-# LANGUAGE TemplateHaskell #-}
21{-# OPTIONS -fno-warn-orphans #-}
22module Network.BitTorrent.Address
23 ( -- * Address
24 Address (..)
25 , fromAddr
26
27 -- ** IP
28 , IPv4
29 , IPv6
30 , IP (..)
31
32 -- * PeerId
33 -- $peer-id
34 , PeerId
35
36 -- ** Generation
37 , genPeerId
38 , timestamp
39 , entropy
40
41 -- ** Encoding
42 , azureusStyle
43 , shadowStyle
44 , defaultClientId
45 , defaultVersionNumber
46
47 -- * PeerAddr
48 -- $peer-addr
49 , PeerAddr(..)
50 , defaultPorts
51 , peerSockAddr
52 , peerSocket
53
54 -- * Node
55 -- ** Id
56 , NodeId
57 , testIdBit
58 , genNodeId
59 , NodeDistance
60 , distance
61
62 -- ** Info
63 , NodeAddr (..)
64 , NodeInfo (..)
65 , rank
66
67 -- * Fingerprint
68 -- $fingerprint
69 , Software (..)
70 , Fingerprint (..)
71 , libFingerprint
72 , fingerprint
73
74 -- * Utils
75 , libUserAgent
76 ) where
77
78import Control.Applicative
79import Control.Monad
80import Data.BEncode as BE
81import Data.BEncode as BS
82import Data.BEncode.BDict (BKey)
83import Data.Bits
84import Data.ByteString as BS
85import Data.ByteString.Internal as BS
86import Data.ByteString.Base16 as Base16
87import Data.ByteString.Char8 as BC
88import Data.ByteString.Char8 as BS8
89import qualified Data.ByteString.Lazy as BL
90import qualified Data.ByteString.Lazy.Builder as BS
91import Data.Char
92import Data.Convertible
93import Data.Default
94import Data.Foldable
95import Data.IP
96import Data.List as L
97import Data.List.Split as L
98import Data.Maybe (fromMaybe, catMaybes)
99import Data.Monoid
100import Data.Hashable
101import Data.Ord
102import Data.Serialize as S
103import Data.String
104import Data.Time
105import Data.Typeable
106import Data.Version
107import Data.Word
108import qualified Text.ParserCombinators.ReadP as RP
109import Text.Read (readMaybe)
110import Network.HTTP.Types.QueryLike
111import Network.Socket
112import Text.PrettyPrint as PP hiding ((<>))
113import Text.PrettyPrint.Class
114import System.Locale (defaultTimeLocale)
115import System.Entropy
116
117-- import Paths_bittorrent (version)
118
119{-----------------------------------------------------------------------
120-- Address
121-----------------------------------------------------------------------}
122
123instance Pretty UTCTime where
124 pretty = PP.text . show
125
126class (Eq a, Serialize a, Typeable a, Hashable a, Pretty a)
127 => Address a where
128 toSockAddr :: a -> SockAddr
129 fromSockAddr :: SockAddr -> Maybe a
130
131fromAddr :: (Address a, Address b) => a -> Maybe b
132fromAddr = fromSockAddr . toSockAddr
133
134-- | Note that port is zeroed.
135instance Address IPv4 where
136 toSockAddr = SockAddrInet 0 . toHostAddress
137 fromSockAddr (SockAddrInet _ h) = Just (fromHostAddress h)
138 fromSockAddr _ = Nothing
139
140-- | Note that port is zeroed.
141instance Address IPv6 where
142 toSockAddr h = SockAddrInet6 0 0 (toHostAddress6 h) 0
143 fromSockAddr (SockAddrInet6 _ _ h _) = Just (fromHostAddress6 h)
144 fromSockAddr _ = Nothing
145
146-- | Note that port is zeroed.
147instance Address IP where
148 toSockAddr (IPv4 h) = toSockAddr h
149 toSockAddr (IPv6 h) = toSockAddr h
150 fromSockAddr sa =
151 IPv4 <$> fromSockAddr sa
152 <|> IPv6 <$> fromSockAddr sa
153
154setPort :: PortNumber -> SockAddr -> SockAddr
155setPort port (SockAddrInet _ h ) = SockAddrInet port h
156setPort port (SockAddrInet6 _ f h s) = SockAddrInet6 port f h s
157setPort _ (SockAddrUnix s ) = SockAddrUnix s
158{-# INLINE setPort #-}
159
160getPort :: SockAddr -> Maybe PortNumber
161getPort (SockAddrInet p _ ) = Just p
162getPort (SockAddrInet6 p _ _ _) = Just p
163getPort (SockAddrUnix _ ) = Nothing
164{-# INLINE getPort #-}
165
166instance Address a => Address (NodeAddr a) where
167 toSockAddr NodeAddr {..} = setPort nodePort $ toSockAddr nodeHost
168 fromSockAddr sa = NodeAddr <$> fromSockAddr sa <*> getPort sa
169
170instance Address a => Address (PeerAddr a) where
171 toSockAddr PeerAddr {..} = setPort peerPort $ toSockAddr peerHost
172 fromSockAddr sa = PeerAddr Nothing <$> fromSockAddr sa <*> getPort sa
173
174{-----------------------------------------------------------------------
175-- Peer id
176-----------------------------------------------------------------------}
177-- $peer-id
178--
179-- 'PeerID' represent self assigned peer identificator. Ideally each
180-- host in the network should have unique peer id to avoid
181-- collisions, therefore for peer ID generation we use good entropy
182-- source. Peer ID is sent in /tracker request/, sent and received in
183-- /peer handshakes/ and used in DHT queries.
184--
185
186-- TODO use unpacked Word160 form (length is known statically)
187
188-- | Peer identifier is exactly 20 bytes long bytestring.
189newtype PeerId = PeerId { getPeerId :: ByteString }
190 deriving (Show, Eq, Ord, BEncode, Typeable)
191
192peerIdLen :: Int
193peerIdLen = 20
194
195-- | For testing purposes only.
196instance Default PeerId where
197 def = azureusStyle defaultClientId defaultVersionNumber ""
198
199instance Hashable PeerId where
200 hashWithSalt = hashUsing getPeerId
201 {-# INLINE hashWithSalt #-}
202
203instance Serialize PeerId where
204 put = putByteString . getPeerId
205 get = PeerId <$> getBytes peerIdLen
206
207instance QueryValueLike PeerId where
208 toQueryValue (PeerId pid) = Just pid
209 {-# INLINE toQueryValue #-}
210
211instance IsString PeerId where
212 fromString str
213 | BS.length bs == peerIdLen = PeerId bs
214 | otherwise = error $ "Peer id should be 20 bytes long: " ++ show str
215 where
216 bs = fromString str
217
218instance Pretty PeerId where
219 pretty = text . BC.unpack . getPeerId
220
221instance Convertible BS.ByteString PeerId where
222 safeConvert bs
223 | BS.length bs == peerIdLen = pure (PeerId bs)
224 | otherwise = convError "invalid length" bs
225
226------------------------------------------------------------------------
227
228-- | Pad bytestring so it's becomes exactly request length. Conversion
229-- is done like so:
230--
231-- * length < size: Complete bytestring by given charaters.
232--
233-- * length = size: Output bytestring as is.
234--
235-- * length > size: Drop last (length - size) charaters from a
236-- given bytestring.
237--
238byteStringPadded :: ByteString -- ^ bytestring to be padded.
239 -> Int -- ^ size of result builder.
240 -> Char -- ^ character used for padding.
241 -> BS.Builder
242byteStringPadded bs s c =
243 BS.byteString (BS.take s bs) <>
244 BS.byteString (BC.replicate padLen c)
245 where
246 padLen = s - min (BS.length bs) s
247
248-- | Azureus-style encoding have the following layout:
249--
250-- * 1 byte : '-'
251--
252-- * 2 bytes: client id
253--
254-- * 4 bytes: version number
255--
256-- * 1 byte : '-'
257--
258-- * 12 bytes: random number
259--
260azureusStyle :: ByteString -- ^ 2 character client ID, padded with 'H'.
261 -> ByteString -- ^ Version number, padded with 'X'.
262 -> ByteString -- ^ Random number, padded with '0'.
263 -> PeerId -- ^ Azureus-style encoded peer ID.
264azureusStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
265 BS.char8 '-' <>
266 byteStringPadded cid 2 'H' <>
267 byteStringPadded ver 4 'X' <>
268 BS.char8 '-' <>
269 byteStringPadded rnd 12 '0'
270
271-- | Shadow-style encoding have the following layout:
272--
273-- * 1 byte : client id.
274--
275-- * 0-4 bytes: version number. If less than 4 then padded with
276-- '-' char.
277--
278-- * 15 bytes : random number. If length is less than 15 then
279-- padded with '0' char.
280--
281shadowStyle :: Char -- ^ Client ID.
282 -> ByteString -- ^ Version number.
283 -> ByteString -- ^ Random number.
284 -> PeerId -- ^ Shadow style encoded peer ID.
285shadowStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
286 BS.char8 cid <>
287 byteStringPadded ver 4 '-' <>
288 byteStringPadded rnd 15 '0'
289
290
291-- | 'HS'- 2 bytes long client identifier.
292defaultClientId :: ByteString
293defaultClientId = "HS"
294
295-- | Gives exactly 4 bytes long version number for any version of the
296-- package. Version is taken from .cabal file.
297defaultVersionNumber :: ByteString
298defaultVersionNumber = BS.take 4 $ BC.pack $ foldMap show $
299 versionBranch myVersion
300 where
301 Fingerprint _ myVersion = libFingerprint
302
303------------------------------------------------------------------------
304
305-- | Gives 15 characters long decimal timestamp such that:
306--
307-- * 6 bytes : first 6 characters from picoseconds obtained with %q.
308--
309-- * 1 byte : character \'.\' for readability.
310--
311-- * 9..* bytes: number of whole seconds since the Unix epoch
312-- (!)REVERSED.
313--
314-- Can be used both with shadow and azureus style encoding. This
315-- format is used to make the ID's readable for debugging purposes.
316--
317timestamp :: IO ByteString
318timestamp = (BC.pack . format) <$> getCurrentTime
319 where
320 format t = L.take 6 (formatTime defaultTimeLocale "%q" t) ++ "." ++
321 L.take 9 (L.reverse (formatTime defaultTimeLocale "%s" t))
322
323-- | Gives 15 character long random bytestring. This is more robust
324-- method for generation of random part of peer ID than 'timestamp'.
325entropy :: IO ByteString
326entropy = getEntropy 15
327
328-- NOTE: entropy generates incorrrect peer id
329
330-- | Here we use 'azureusStyle' encoding with the following args:
331--
332-- * 'HS' for the client id; ('defaultClientId')
333--
334-- * Version of the package for the version number;
335-- ('defaultVersionNumber')
336--
337-- * UTC time day ++ day time for the random number. ('timestamp')
338--
339genPeerId :: IO PeerId
340genPeerId = azureusStyle defaultClientId defaultVersionNumber <$> timestamp
341
342{-----------------------------------------------------------------------
343-- Peer Addr
344-----------------------------------------------------------------------}
345-- $peer-addr
346--
347-- 'PeerAddr' is used to represent peer address. Currently it's
348-- just peer IP and peer port but this might change in future.
349--
350
351{-----------------------------------------------------------------------
352-- Port number
353-----------------------------------------------------------------------}
354
355instance BEncode PortNumber where
356 toBEncode = toBEncode . fromEnum
357 fromBEncode = fromBEncode >=> portNumber
358 where
359 portNumber :: Integer -> BS.Result PortNumber
360 portNumber n
361 | 0 <= n && n <= fromIntegral (maxBound :: Word16)
362 = pure $ fromIntegral n
363 | otherwise = decodingError $ "PortNumber: " ++ show n
364
365instance Serialize PortNumber where
366 get = fromIntegral <$> getWord16be
367 {-# INLINE get #-}
368 put = putWord16be . fromIntegral
369 {-# INLINE put #-}
370
371instance Hashable PortNumber where
372 hashWithSalt s = hashWithSalt s . fromEnum
373 {-# INLINE hashWithSalt #-}
374
375instance Pretty PortNumber where
376 pretty = PP.int . fromEnum
377 {-# INLINE pretty #-}
378
379{-----------------------------------------------------------------------
380-- IP addr
381-----------------------------------------------------------------------}
382
383class IPAddress i where
384 toHostAddr :: i -> Either HostAddress HostAddress6
385
386instance IPAddress IPv4 where
387 toHostAddr = Left . toHostAddress
388 {-# INLINE toHostAddr #-}
389
390instance IPAddress IPv6 where
391 toHostAddr = Right . toHostAddress6
392 {-# INLINE toHostAddr #-}
393
394instance IPAddress IP where
395 toHostAddr (IPv4 ip) = toHostAddr ip
396 toHostAddr (IPv6 ip) = toHostAddr ip
397 {-# INLINE toHostAddr #-}
398
399deriving instance Typeable IP
400deriving instance Typeable IPv4
401deriving instance Typeable IPv6
402
403ipToBEncode :: Show i => i -> BValue
404ipToBEncode ip = BString $ BS8.pack $ show ip
405{-# INLINE ipToBEncode #-}
406
407ipFromBEncode :: Read a => BValue -> BS.Result a
408ipFromBEncode (BString (BS8.unpack -> ipStr))
409 | Just ip <- readMaybe (ipStr) = pure ip
410 | otherwise = decodingError $ "IP: " ++ ipStr
411ipFromBEncode _ = decodingError $ "IP: addr should be a bstring"
412
413instance BEncode IP where
414 toBEncode = ipToBEncode
415 {-# INLINE toBEncode #-}
416 fromBEncode = ipFromBEncode
417 {-# INLINE fromBEncode #-}
418
419instance BEncode IPv4 where
420 toBEncode = ipToBEncode
421 {-# INLINE toBEncode #-}
422 fromBEncode = ipFromBEncode
423 {-# INLINE fromBEncode #-}
424
425instance BEncode IPv6 where
426 toBEncode = ipToBEncode
427 {-# INLINE toBEncode #-}
428 fromBEncode = ipFromBEncode
429 {-# INLINE fromBEncode #-}
430
431-- | When 'get'ing an IP it must be 'isolate'd to the appropriate
432-- number of bytes since we have no other way of telling which
433-- address type we are trying to parse
434instance Serialize IP where
435 put (IPv4 ip) = put ip
436 put (IPv6 ip) = put ip
437
438 get = do
439 n <- remaining
440 case n of
441 4 -> IPv4 <$> get
442 16 -> IPv6 <$> get
443 _ -> fail "Wrong number of bytes remaining to parse IP"
444
445instance Serialize IPv4 where
446 put = putWord32host . toHostAddress
447 get = fromHostAddress <$> getWord32host
448
449instance Serialize IPv6 where
450 put ip = put $ toHostAddress6 ip
451 get = fromHostAddress6 <$> get
452
453instance Pretty IPv4 where
454 pretty = PP.text . show
455 {-# INLINE pretty #-}
456
457instance Pretty IPv6 where
458 pretty = PP.text . show
459 {-# INLINE pretty #-}
460
461instance Pretty IP where
462 pretty = PP.text . show
463 {-# INLINE pretty #-}
464
465instance Hashable IPv4 where
466 hashWithSalt = hashUsing toHostAddress
467 {-# INLINE hashWithSalt #-}
468
469instance Hashable IPv6 where
470 hashWithSalt s a = hashWithSalt s (toHostAddress6 a)
471
472instance Hashable IP where
473 hashWithSalt s (IPv4 h) = hashWithSalt s h
474 hashWithSalt s (IPv6 h) = hashWithSalt s h
475
476{-----------------------------------------------------------------------
477-- Peer addr
478-----------------------------------------------------------------------}
479-- TODO check semantic of ord and eq instances
480
481-- | Peer address info normally extracted from peer list or peer
482-- compact list encoding.
483data PeerAddr a = PeerAddr
484 { peerId :: !(Maybe PeerId)
485
486 -- | This is usually 'IPv4', 'IPv6', 'IP' or unresolved
487 -- 'HostName'.
488 , peerHost :: !a
489
490 -- | The port the peer listenning for incoming P2P sessions.
491 , peerPort :: {-# UNPACK #-} !PortNumber
492 } deriving (Show, Eq, Ord, Typeable, Functor)
493
494peer_ip_key, peer_id_key, peer_port_key :: BKey
495peer_ip_key = "ip"
496peer_id_key = "peer id"
497peer_port_key = "port"
498
499-- | The tracker's 'announce response' compatible encoding.
500instance (Typeable a, BEncode a) => BEncode (PeerAddr a) where
501 toBEncode PeerAddr {..} = toDict $
502 peer_ip_key .=! peerHost
503 .: peer_id_key .=? peerId
504 .: peer_port_key .=! peerPort
505 .: endDict
506
507 fromBEncode = fromDict $ do
508 peerAddr <$>! peer_ip_key
509 <*>? peer_id_key
510 <*>! peer_port_key
511 where
512 peerAddr = flip PeerAddr
513
514-- | The tracker's 'compact peer list' compatible encoding. The
515-- 'peerId' is always 'Nothing'.
516--
517-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
518--
519-- TODO: test byte order
520instance (Serialize a) => Serialize (PeerAddr a) where
521 put PeerAddr {..} = put peerHost >> put peerPort
522 get = PeerAddr Nothing <$> get <*> get
523
524-- | @127.0.0.1:6881@
525instance Default (PeerAddr IPv4) where
526 def = "127.0.0.1:6881"
527
528-- | @127.0.0.1:6881@
529instance Default (PeerAddr IP) where
530 def = IPv4 <$> def
531
532-- | Example:
533--
534-- @peerPort \"127.0.0.1:6881\" == 6881@
535--
536instance IsString (PeerAddr IPv4) where
537 fromString str
538 | [hostAddrStr, portStr] <- splitWhen (== ':') str
539 , Just hostAddr <- readMaybe hostAddrStr
540 , Just portNum <- toEnum <$> readMaybe portStr
541 = PeerAddr Nothing hostAddr portNum
542 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv4): " ++ str
543
544instance Read (PeerAddr IPv4) where
545 readsPrec i = RP.readP_to_S $ do
546 ipv4 <- RP.readS_to_P (readsPrec i)
547 _ <- RP.char ':'
548 port <- toEnum <$> RP.readS_to_P (readsPrec i)
549 return $ PeerAddr Nothing ipv4 port
550
551readsIPv6_port :: String -> [((IPv6, PortNumber), String)]
552readsIPv6_port = RP.readP_to_S $ do
553 ip <- RP.char '[' *> (RP.readS_to_P reads) <* RP.char ']'
554 _ <- RP.char ':'
555 port <- toEnum <$> read <$> (RP.many1 $ RP.satisfy isDigit) <* RP.eof
556 return (ip,port)
557
558instance IsString (PeerAddr IPv6) where
559 fromString str
560 | [((ip,port),"")] <- readsIPv6_port str =
561 PeerAddr Nothing ip port
562 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv6): " ++ str
563
564instance IsString (PeerAddr IP) where
565 fromString str
566 | '[' `L.elem` str = IPv6 <$> fromString str
567 | otherwise = IPv4 <$> fromString str
568
569-- | fingerprint + "at" + dotted.host.inet.addr:port
570-- TODO: instances for IPv6, HostName
571instance Pretty a => Pretty (PeerAddr a) where
572 pretty PeerAddr {..}
573 | Just pid <- peerId = pretty (fingerprint pid) <+> "at" <+> paddr
574 | otherwise = paddr
575 where
576 paddr = pretty peerHost <> ":" <> text (show peerPort)
577
578instance Hashable a => Hashable (PeerAddr a) where
579 hashWithSalt s PeerAddr {..} =
580 s `hashWithSalt` peerId `hashWithSalt` peerHost `hashWithSalt` peerPort
581
582-- | Ports typically reserved for bittorrent P2P listener.
583defaultPorts :: [PortNumber]
584defaultPorts = [6881..6889]
585
586_resolvePeerAddr :: (IPAddress i) => PeerAddr HostName -> PeerAddr i
587_resolvePeerAddr = undefined
588
589_peerSockAddr :: PeerAddr IP -> (Family, SockAddr)
590_peerSockAddr PeerAddr {..} =
591 case peerHost of
592 IPv4 ipv4 ->
593 (AF_INET, SockAddrInet peerPort (toHostAddress ipv4))
594 IPv6 ipv6 ->
595 (AF_INET6, SockAddrInet6 peerPort 0 (toHostAddress6 ipv6) 0)
596
597peerSockAddr :: PeerAddr IP -> SockAddr
598peerSockAddr = snd . _peerSockAddr
599
600-- | Create a socket connected to the address specified in a peerAddr
601peerSocket :: SocketType -> PeerAddr IP -> IO Socket
602peerSocket socketType pa = do
603 let (family, addr) = _peerSockAddr pa
604 sock <- socket family socketType defaultProtocol
605 connect sock addr
606 return sock
607
608{-----------------------------------------------------------------------
609-- Node info
610-----------------------------------------------------------------------}
611-- $node-info
612--
613-- A \"node\" is a client\/server listening on a UDP port
614-- implementing the distributed hash table protocol. The DHT is
615-- composed of nodes and stores the location of peers. BitTorrent
616-- clients include a DHT node, which is used to contact other nodes
617-- in the DHT to get the location of peers to download from using
618-- the BitTorrent protocol.
619
620-- TODO more compact representation ('ShortByteString's?)
621
622-- | Each node has a globally unique identifier known as the \"node
623-- ID.\"
624--
625-- Normally, /this/ node id should be saved between invocations
626-- of the client software.
627newtype NodeId = NodeId ByteString
628 deriving (Show, Eq, Ord, BEncode, Typeable)
629
630nodeIdSize :: Int
631nodeIdSize = 20
632
633-- | Meaningless node id, for testing purposes only.
634instance Default NodeId where
635 def = NodeId (BS.replicate nodeIdSize 0)
636
637instance Serialize NodeId where
638 get = NodeId <$> getByteString nodeIdSize
639 {-# INLINE get #-}
640 put (NodeId bs) = putByteString bs
641 {-# INLINE put #-}
642
643-- | ASCII encoded.
644instance IsString NodeId where
645 fromString str
646 | L.length str == nodeIdSize = NodeId (fromString str)
647 | otherwise = error "fromString: invalid NodeId length"
648 {-# INLINE fromString #-}
649
650-- | base16 encoded.
651instance Pretty NodeId where
652 pretty (NodeId nid) = PP.text $ BC.unpack $ Base16.encode nid
653
654-- | Test if the nth bit is set.
655testIdBit :: NodeId -> Word -> Bool
656testIdBit (NodeId bs) i
657 | fromIntegral i < nodeIdSize * 8
658 , (q, r) <- quotRem (fromIntegral i) 8
659 = testBit (BS.index bs q) r
660 | otherwise = False
661{-# INLINE testIdBit #-}
662
663-- TODO WARN is the 'system' random suitable for this?
664-- | Generate random NodeID used for the entire session.
665-- Distribution of ID's should be as uniform as possible.
666--
667genNodeId :: IO NodeId
668genNodeId = NodeId <$> getEntropy nodeIdSize
669
670------------------------------------------------------------------------
671
672-- | In Kademlia, the distance metric is XOR and the result is
673-- interpreted as an unsigned integer.
674newtype NodeDistance = NodeDistance BS.ByteString
675 deriving (Eq, Ord)
676
677instance Pretty NodeDistance where
678 pretty (NodeDistance bs) = foldMap bitseq $ BS.unpack bs
679 where
680 listBits w = L.map (testBit w) (L.reverse [0..bitSize w - 1])
681 bitseq = foldMap (int . fromEnum) . listBits
682
683-- | distance(A,B) = |A xor B| Smaller values are closer.
684distance :: NodeId -> NodeId -> NodeDistance
685distance (NodeId a) (NodeId b) = NodeDistance (BS.pack (BS.zipWith xor a b))
686
687------------------------------------------------------------------------
688
689data NodeAddr a = NodeAddr
690 { nodeHost :: !a
691 , nodePort :: {-# UNPACK #-} !PortNumber
692 } deriving (Eq, Typeable, Functor)
693
694instance Show a => Show (NodeAddr a) where
695 showsPrec i NodeAddr {..}
696 = showsPrec i nodeHost <> showString ":" <> showsPrec i nodePort
697
698instance Read (NodeAddr IPv4) where
699 readsPrec i x = [ (fromPeerAddr a, s) | (a, s) <- readsPrec i x ]
700
701-- | @127.0.0.1:6882@
702instance Default (NodeAddr IPv4) where
703 def = "127.0.0.1:6882"
704
705-- | KRPC compatible encoding.
706instance Serialize a => Serialize (NodeAddr a) where
707 get = NodeAddr <$> get <*> get
708 {-# INLINE get #-}
709 put NodeAddr {..} = put nodeHost >> put nodePort
710 {-# INLINE put #-}
711
712-- | Torrent file compatible encoding.
713instance BEncode a => BEncode (NodeAddr a) where
714 toBEncode NodeAddr {..} = toBEncode (nodeHost, nodePort)
715 {-# INLINE toBEncode #-}
716 fromBEncode b = uncurry NodeAddr <$> fromBEncode b
717 {-# INLINE fromBEncode #-}
718
719instance Hashable a => Hashable (NodeAddr a) where
720 hashWithSalt s NodeAddr {..} = hashWithSalt s (nodeHost, nodePort)
721 {-# INLINE hashWithSalt #-}
722
723instance Pretty ip => Pretty (NodeAddr ip) where
724 pretty NodeAddr {..} = pretty nodeHost <> ":" <> pretty nodePort
725
726-- | Example:
727--
728-- @nodePort \"127.0.0.1:6881\" == 6881@
729--
730instance IsString (NodeAddr IPv4) where
731 fromString = fromPeerAddr . fromString
732
733fromPeerAddr :: PeerAddr a -> NodeAddr a
734fromPeerAddr PeerAddr {..} = NodeAddr
735 { nodeHost = peerHost
736 , nodePort = peerPort
737 }
738
739------------------------------------------------------------------------
740
741data NodeInfo a = NodeInfo
742 { nodeId :: !NodeId
743 , nodeAddr :: !(NodeAddr a)
744 } deriving (Show, Eq, Functor)
745
746instance Eq a => Ord (NodeInfo a) where
747 compare = comparing nodeId
748
749-- | KRPC 'compact list' compatible encoding: contact information for
750-- nodes is encoded as a 26-byte string. Also known as "Compact node
751-- info" the 20-byte Node ID in network byte order has the compact
752-- IP-address/port info concatenated to the end.
753instance Serialize a => Serialize (NodeInfo a) where
754 get = NodeInfo <$> get <*> get
755 put NodeInfo {..} = put nodeId >> put nodeAddr
756
757instance Pretty ip => Pretty (NodeInfo ip) where
758 pretty NodeInfo {..} = pretty nodeId <> "@(" <> pretty nodeAddr <> ")"
759
760instance Pretty ip => Pretty [NodeInfo ip] where
761 pretty = PP.vcat . PP.punctuate "," . L.map pretty
762
763-- | Order by closeness: nearest nodes first.
764rank :: Eq ip => NodeId -> [NodeInfo ip] -> [NodeInfo ip]
765rank nid = L.sortBy (comparing (distance nid . nodeId))
766
767{-----------------------------------------------------------------------
768-- Fingerprint
769-----------------------------------------------------------------------}
770-- $fingerprint
771--
772-- 'Fingerprint' is used to identify the client implementation and
773-- version which also contained in 'Peer'. For exsample first 6
774-- bytes of peer id of this this library are @-HS0100-@ while for
775-- mainline we have @M4-3-6--@. We could extract this info and
776-- print in human-friendly form: this is useful for debugging and
777-- logging.
778--
779-- For more information see:
780-- <http://bittorrent.org/beps/bep_0020.html>
781--
782--
783-- NOTE: Do /not/ use this information to control client
784-- capabilities (such as supported enchancements), this should be
785-- done using 'Network.BitTorrent.Extension'!
786--
787
788-- TODO FIXME
789version :: Version
790version = Version [0, 0, 0, 3] []
791
792-- | List of registered client versions + 'IlibHSbittorrent' (this
793-- package) + 'IUnknown' (for not recognized software). All names are
794-- prefixed by \"I\" because some of them starts from lowercase letter
795-- but that is not a valid Haskell constructor name.
796--
797data Software =
798 IUnknown
799
800 | IMainline
801
802 | IABC
803 | IOspreyPermaseed
804 | IBTQueue
805 | ITribler
806 | IShadow
807 | IBitTornado
808
809-- UPnP(!) Bit Torrent !???
810-- 'U' - UPnP NAT Bit Torrent
811 | IBitLord
812 | IOpera
813 | IMLdonkey
814
815 | IAres
816 | IArctic
817 | IAvicora
818 | IBitPump
819 | IAzureus
820 | IBitBuddy
821 | IBitComet
822 | IBitflu
823 | IBTG
824 | IBitRocket
825 | IBTSlave
826 | IBittorrentX
827 | IEnhancedCTorrent
828 | ICTorrent
829 | IDelugeTorrent
830 | IPropagateDataClient
831 | IEBit
832 | IElectricSheep
833 | IFoxTorrent
834 | IGSTorrent
835 | IHalite
836 | IlibHSbittorrent
837 | IHydranode
838 | IKGet
839 | IKTorrent
840 | ILH_ABC
841 | ILphant
842 | ILibtorrent
843 | ILibTorrent
844 | ILimeWire
845 | IMonoTorrent
846 | IMooPolice
847 | IMiro
848 | IMoonlightTorrent
849 | INetTransport
850 | IPando
851 | IqBittorrent
852 | IQQDownload
853 | IQt4TorrentExample
854 | IRetriever
855 | IShareaza
856 | ISwiftbit
857 | ISwarmScope
858 | ISymTorrent
859 | Isharktorrent
860 | ITorrentDotNET
861 | ITransmission
862 | ITorrentstorm
863 | ITuoTu
864 | IuLeecher
865 | IuTorrent
866 | IVagaa
867 | IBitLet
868 | IFireTorrent
869 | IXunlei
870 | IXanTorrent
871 | IXtorrent
872 | IZipTorrent
873 deriving (Show, Eq, Ord, Enum, Bounded)
874
875parseSoftware :: ByteString -> Software
876parseSoftware = f . BC.unpack
877 where
878 f "AG" = IAres
879 f "A~" = IAres
880 f "AR" = IArctic
881 f "AV" = IAvicora
882 f "AX" = IBitPump
883 f "AZ" = IAzureus
884 f "BB" = IBitBuddy
885 f "BC" = IBitComet
886 f "BF" = IBitflu
887 f "BG" = IBTG
888 f "BR" = IBitRocket
889 f "BS" = IBTSlave
890 f "BX" = IBittorrentX
891 f "CD" = IEnhancedCTorrent
892 f "CT" = ICTorrent
893 f "DE" = IDelugeTorrent
894 f "DP" = IPropagateDataClient
895 f "EB" = IEBit
896 f "ES" = IElectricSheep
897 f "FT" = IFoxTorrent
898 f "GS" = IGSTorrent
899 f "HL" = IHalite
900 f "HS" = IlibHSbittorrent
901 f "HN" = IHydranode
902 f "KG" = IKGet
903 f "KT" = IKTorrent
904 f "LH" = ILH_ABC
905 f "LP" = ILphant
906 f "LT" = ILibtorrent
907 f "lt" = ILibTorrent
908 f "LW" = ILimeWire
909 f "MO" = IMonoTorrent
910 f "MP" = IMooPolice
911 f "MR" = IMiro
912 f "ML" = IMLdonkey
913 f "MT" = IMoonlightTorrent
914 f "NX" = INetTransport
915 f "PD" = IPando
916 f "qB" = IqBittorrent
917 f "QD" = IQQDownload
918 f "QT" = IQt4TorrentExample
919 f "RT" = IRetriever
920 f "S~" = IShareaza
921 f "SB" = ISwiftbit
922 f "SS" = ISwarmScope
923 f "ST" = ISymTorrent
924 f "st" = Isharktorrent
925 f "SZ" = IShareaza
926 f "TN" = ITorrentDotNET
927 f "TR" = ITransmission
928 f "TS" = ITorrentstorm
929 f "TT" = ITuoTu
930 f "UL" = IuLeecher
931 f "UT" = IuTorrent
932 f "VG" = IVagaa
933 f "WT" = IBitLet
934 f "WY" = IFireTorrent
935 f "XL" = IXunlei
936 f "XT" = IXanTorrent
937 f "XX" = IXtorrent
938 f "ZT" = IZipTorrent
939 f _ = IUnknown
940
941-- | Used to represent a not recognized implementation
942instance Default Software where
943 def = IUnknown
944 {-# INLINE def #-}
945
946-- | Example: @\"BitLet\" == 'IBitLet'@
947instance IsString Software where
948 fromString str
949 | Just impl <- L.lookup str alist = impl
950 | otherwise = error $ "fromString: not recognized " ++ str
951 where
952 alist = L.map mk [minBound..maxBound]
953 mk x = (L.tail $ show x, x)
954
955-- | Example: @pretty 'IBitLet' == \"IBitLet\"@
956instance Pretty Software where
957 pretty = text . L.tail . show
958
959-- | Just the '0' version.
960instance Default Version where
961 def = Version [0] []
962 {-# INLINE def #-}
963
964-- | For dot delimited version strings.
965-- Example: @fromString \"0.1.0.2\" == Version [0, 1, 0, 2]@
966--
967instance IsString Version where
968 fromString str
969 | Just nums <- chunkNums str = Version nums []
970 | otherwise = error $ "fromString: invalid version string " ++ str
971 where
972 chunkNums = sequence . L.map readMaybe . L.linesBy ('.' ==)
973
974instance Pretty Version where
975 pretty = text . showVersion
976
977-- | The all sensible infomation that can be obtained from a peer
978-- identifier or torrent /createdBy/ field.
979data Fingerprint = Fingerprint Software Version
980 deriving (Show, Eq, Ord)
981
982-- | Unrecognized client implementation.
983instance Default Fingerprint where
984 def = Fingerprint def def
985 {-# INLINE def #-}
986
987-- | Example: @\"BitComet-1.2\" == ClientInfo IBitComet (Version [1, 2] [])@
988instance IsString Fingerprint where
989 fromString str
990 | _ : ver <- _ver = Fingerprint (fromString impl) (fromString ver)
991 | otherwise = error $ "fromString: invalid client info string" ++ str
992 where
993 (impl, _ver) = L.span ((/=) '-') str
994
995instance Pretty Fingerprint where
996 pretty (Fingerprint s v) = pretty s <+> "version" <+> pretty v
997
998-- | Fingerprint of this (the bittorrent library) package. Normally,
999-- applications should introduce its own fingerprints, otherwise they
1000-- can use 'libFingerprint' value.
1001--
1002libFingerprint :: Fingerprint
1003libFingerprint = Fingerprint IlibHSbittorrent version
1004
1005-- | HTTP user agent of this (the bittorrent library) package. Can be
1006-- used in HTTP tracker requests.
1007libUserAgent :: String
1008libUserAgent = render (pretty IlibHSbittorrent <> "/" <> pretty version)
1009
1010{-----------------------------------------------------------------------
1011-- For torrent file
1012-----------------------------------------------------------------------}
1013-- TODO collect information about createdBy torrent field
1014{-
1015renderImpl :: ClientImpl -> Text
1016renderImpl = T.pack . L.tail . show
1017
1018renderVersion :: Version -> Text
1019renderVersion = undefined
1020
1021renderClientInfo :: ClientInfo -> Text
1022renderClientInfo ClientInfo {..} = renderImpl ciImpl <> "/" <> renderVersion ciVersion
1023
1024parseClientInfo :: Text -> ClientImpl
1025parseClientInfo t = undefined
1026-}
1027{-
1028-- code used for generation; remove it later on
1029
1030mkEnumTyDef :: NM -> String
1031mkEnumTyDef = unlines . map (" | I" ++) . nub . map snd
1032
1033mkPars :: NM -> String
1034mkPars = unlines . map (\(code, impl) -> " f \"" ++ code ++ "\" = " ++ "I" ++ impl)
1035
1036type NM = [(String, String)]
1037nameMap :: NM
1038nameMap =
1039 [ ("AG", "Ares")
1040 , ("A~", "Ares")
1041 , ("AR", "Arctic")
1042 , ("AV", "Avicora")
1043 , ("AX", "BitPump")
1044 , ("AZ", "Azureus")
1045 , ("BB", "BitBuddy")
1046 , ("BC", "BitComet")
1047 , ("BF", "Bitflu")
1048 , ("BG", "BTG")
1049 , ("BR", "BitRocket")
1050 , ("BS", "BTSlave")
1051 , ("BX", "BittorrentX")
1052 , ("CD", "EnhancedCTorrent")
1053 , ("CT", "CTorrent")
1054 , ("DE", "DelugeTorrent")
1055 , ("DP", "PropagateDataClient")
1056 , ("EB", "EBit")
1057 , ("ES", "ElectricSheep")
1058 , ("FT", "FoxTorrent")
1059 , ("GS", "GSTorrent")
1060 , ("HL", "Halite")
1061 , ("HS", "libHSnetwork_bittorrent")
1062 , ("HN", "Hydranode")
1063 , ("KG", "KGet")
1064 , ("KT", "KTorrent")
1065 , ("LH", "LH_ABC")
1066 , ("LP", "Lphant")
1067 , ("LT", "Libtorrent")
1068 , ("lt", "LibTorrent")
1069 , ("LW", "LimeWire")
1070 , ("MO", "MonoTorrent")
1071 , ("MP", "MooPolice")
1072 , ("MR", "Miro")
1073 , ("MT", "MoonlightTorrent")
1074 , ("NX", "NetTransport")
1075 , ("PD", "Pando")
1076 , ("qB", "qBittorrent")
1077 , ("QD", "QQDownload")
1078 , ("QT", "Qt4TorrentExample")
1079 , ("RT", "Retriever")
1080 , ("S~", "Shareaza")
1081 , ("SB", "Swiftbit")
1082 , ("SS", "SwarmScope")
1083 , ("ST", "SymTorrent")
1084 , ("st", "sharktorrent")
1085 , ("SZ", "Shareaza")
1086 , ("TN", "TorrentDotNET")
1087 , ("TR", "Transmission")
1088 , ("TS", "Torrentstorm")
1089 , ("TT", "TuoTu")
1090 , ("UL", "uLeecher")
1091 , ("UT", "uTorrent")
1092 , ("VG", "Vagaa")
1093 , ("WT", "BitLet")
1094 , ("WY", "FireTorrent")
1095 , ("XL", "Xunlei")
1096 , ("XT", "XanTorrent")
1097 , ("XX", "Xtorrent")
1098 , ("ZT", "ZipTorrent")
1099 ]
1100-}
1101
1102-- TODO use regexps
1103
1104-- | Tries to extract meaningful information from peer ID bytes. If
1105-- peer id uses unknown coding style then client info returned is
1106-- 'def'.
1107--
1108fingerprint :: PeerId -> Fingerprint
1109fingerprint pid = either (const def) id $ runGet getCI (getPeerId pid)
1110 where
1111 getCI = do
1112 leading <- BS.w2c <$> getWord8
1113 case leading of
1114 '-' -> Fingerprint <$> getAzureusImpl <*> getAzureusVersion
1115 'M' -> Fingerprint <$> pure IMainline <*> getMainlineVersion
1116 'e' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
1117 'F' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
1118 c -> do
1119 c1 <- w2c <$> S.lookAhead getWord8
1120 if c1 == 'P'
1121 then do
1122 _ <- getWord8
1123 Fingerprint <$> pure IOpera <*> getOperaVersion
1124 else Fingerprint <$> pure (getShadowImpl c) <*> getShadowVersion
1125
1126 getMainlineVersion = do
1127 str <- BC.unpack <$> getByteString 7
1128 let mnums = L.filter (not . L.null) $ L.linesBy ('-' ==) str
1129 return $ Version (fromMaybe [] $ sequence $ L.map readMaybe mnums) []
1130
1131 getAzureusImpl = parseSoftware <$> getByteString 2
1132 getAzureusVersion = mkVer <$> getByteString 4
1133 where
1134 mkVer bs = Version [fromMaybe 0 $ readMaybe $ BC.unpack bs] []
1135
1136 getBitCometImpl = do
1137 bs <- getByteString 3
1138 S.lookAhead $ do
1139 _ <- getByteString 2
1140 lr <- getByteString 4
1141 return $
1142 if lr == "LORD" then IBitLord else
1143 if bs == "UTB" then IBitComet else
1144 if bs == "xbc" then IBitComet else def
1145
1146 getBitCometVersion = do
1147 x <- getWord8
1148 y <- getWord8
1149 return $ Version [fromIntegral x, fromIntegral y] []
1150
1151 getOperaVersion = do
1152 str <- BC.unpack <$> getByteString 4
1153 return $ Version [fromMaybe 0 $ readMaybe str] []
1154
1155 getShadowImpl 'A' = IABC
1156 getShadowImpl 'O' = IOspreyPermaseed
1157 getShadowImpl 'Q' = IBTQueue
1158 getShadowImpl 'R' = ITribler
1159 getShadowImpl 'S' = IShadow
1160 getShadowImpl 'T' = IBitTornado
1161 getShadowImpl _ = IUnknown
1162
1163 decodeShadowVerNr :: Char -> Maybe Int
1164 decodeShadowVerNr c
1165 | '0' < c && c <= '9' = Just (fromEnum c - fromEnum '0')
1166 | 'A' < c && c <= 'Z' = Just ((fromEnum c - fromEnum 'A') + 10)
1167 | 'a' < c && c <= 'z' = Just ((fromEnum c - fromEnum 'a') + 36)
1168 | otherwise = Nothing
1169
1170 getShadowVersion = do
1171 str <- BC.unpack <$> getByteString 5
1172 return $ Version (catMaybes $ L.map decodeShadowVerNr str) []
diff --git a/src/Network/BitTorrent/Client.hs b/src/Network/BitTorrent/Client.hs
index bf6740c3..d21b4d1e 100644
--- a/src/Network/BitTorrent/Client.hs
+++ b/src/Network/BitTorrent/Client.hs
@@ -61,11 +61,9 @@ import Data.Text
61import Network 61import Network
62 62
63import Data.Torrent 63import Data.Torrent
64import Data.Torrent.InfoHash 64import Network.BitTorrent.Address
65import Data.Torrent.Magnet
66import Network.BitTorrent.Client.Types 65import Network.BitTorrent.Client.Types
67import Network.BitTorrent.Client.Handle 66import Network.BitTorrent.Client.Handle
68import Network.BitTorrent.Core
69import Network.BitTorrent.DHT as DHT hiding (Options) 67import Network.BitTorrent.DHT as DHT hiding (Options)
70import Network.BitTorrent.Tracker as Tracker hiding (Options) 68import Network.BitTorrent.Tracker as Tracker hiding (Options)
71import Network.BitTorrent.Exchange as Exchange hiding (Options) 69import Network.BitTorrent.Exchange as Exchange hiding (Options)
diff --git a/src/Network/BitTorrent/Client/Handle.hs b/src/Network/BitTorrent/Client/Handle.hs
index 0d1b7f92..66baac48 100644
--- a/src/Network/BitTorrent/Client/Handle.hs
+++ b/src/Network/BitTorrent/Client/Handle.hs
@@ -26,8 +26,6 @@ import Data.List as L
26import Data.HashMap.Strict as HM 26import Data.HashMap.Strict as HM
27 27
28import Data.Torrent 28import Data.Torrent
29import Data.Torrent.InfoHash
30import Data.Torrent.Magnet
31import Network.BitTorrent.Client.Types as Types 29import Network.BitTorrent.Client.Types as Types
32import Network.BitTorrent.DHT as DHT 30import Network.BitTorrent.DHT as DHT
33import Network.BitTorrent.Exchange as Exchange 31import Network.BitTorrent.Exchange as Exchange
diff --git a/src/Network/BitTorrent/Client/Types.hs b/src/Network/BitTorrent/Client/Types.hs
index c019bc5f..9bae7dc3 100644
--- a/src/Network/BitTorrent/Client/Types.hs
+++ b/src/Network/BitTorrent/Client/Types.hs
@@ -34,9 +34,9 @@ import Data.Ord
34import Network 34import Network
35import System.Log.FastLogger 35import System.Log.FastLogger
36 36
37import Data.Torrent.InfoHash 37import Data.Torrent
38import Network.BitTorrent.Address
38import Network.BitTorrent.Internal.Types as Types 39import Network.BitTorrent.Internal.Types as Types
39import Network.BitTorrent.Core
40import Network.BitTorrent.DHT as DHT 40import Network.BitTorrent.DHT as DHT
41import Network.BitTorrent.Exchange as Exchange 41import Network.BitTorrent.Exchange as Exchange
42import Network.BitTorrent.Tracker as Tracker hiding (Event) 42import Network.BitTorrent.Tracker as Tracker hiding (Event)
@@ -100,7 +100,7 @@ externalAddr Client {..} = PeerAddr
100newtype BitTorrent a = BitTorrent 100newtype BitTorrent a = BitTorrent
101 { unBitTorrent :: ReaderT Client IO a 101 { unBitTorrent :: ReaderT Client IO a
102 } deriving ( Functor, Applicative, Monad 102 } deriving ( Functor, Applicative, Monad
103 , MonadIO, MonadThrow, MonadUnsafeIO, MonadBase IO 103 , MonadIO, MonadThrow, MonadBase IO
104 ) 104 )
105 105
106class MonadBitTorrent m where 106class MonadBitTorrent m where
diff --git a/src/Network/BitTorrent/Core.hs b/src/Network/BitTorrent/Core.hs
deleted file mode 100644
index b9b3c065..00000000
--- a/src/Network/BitTorrent/Core.hs
+++ /dev/null
@@ -1,88 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Re-export every @Network.BitTorrent.Core.*@ module.
9--
10module Network.BitTorrent.Core
11 ( module Core
12
13 -- * Address class
14 , Address (..)
15 , fromAddr
16
17 -- * Re-exports from Data.IP
18 , IPv4
19 , IPv6
20 , IP (..)
21 ) where
22
23import Control.Applicative
24import Data.IP
25import Data.Hashable
26import Data.Serialize
27import Data.Time
28import Data.Typeable
29import Network.Socket (SockAddr (..), PortNumber)
30import Text.PrettyPrint as PP hiding ((<>))
31import Text.PrettyPrint.Class
32
33import Network.BitTorrent.Core.Fingerprint as Core
34import Network.BitTorrent.Core.NodeInfo as Core
35import Network.BitTorrent.Core.PeerId as Core
36import Network.BitTorrent.Core.PeerAddr as Core
37
38
39instance Pretty UTCTime where
40 pretty = PP.text . show
41
42class (Eq a, Serialize a, Typeable a, Hashable a, Pretty a)
43 => Address a where
44 toSockAddr :: a -> SockAddr
45 fromSockAddr :: SockAddr -> Maybe a
46
47fromAddr :: (Address a, Address b) => a -> Maybe b
48fromAddr = fromSockAddr . toSockAddr
49
50-- | Note that port is zeroed.
51instance Address IPv4 where
52 toSockAddr = SockAddrInet 0 . toHostAddress
53 fromSockAddr (SockAddrInet _ h) = Just (fromHostAddress h)
54 fromSockAddr _ = Nothing
55
56-- | Note that port is zeroed.
57instance Address IPv6 where
58 toSockAddr h = SockAddrInet6 0 0 (toHostAddress6 h) 0
59 fromSockAddr (SockAddrInet6 _ _ h _) = Just (fromHostAddress6 h)
60 fromSockAddr _ = Nothing
61
62-- | Note that port is zeroed.
63instance Address IP where
64 toSockAddr (IPv4 h) = toSockAddr h
65 toSockAddr (IPv6 h) = toSockAddr h
66 fromSockAddr sa =
67 IPv4 <$> fromSockAddr sa
68 <|> IPv6 <$> fromSockAddr sa
69
70setPort :: PortNumber -> SockAddr -> SockAddr
71setPort port (SockAddrInet _ h ) = SockAddrInet port h
72setPort port (SockAddrInet6 _ f h s) = SockAddrInet6 port f h s
73setPort _ (SockAddrUnix s ) = SockAddrUnix s
74{-# INLINE setPort #-}
75
76getPort :: SockAddr -> Maybe PortNumber
77getPort (SockAddrInet p _ ) = Just p
78getPort (SockAddrInet6 p _ _ _) = Just p
79getPort (SockAddrUnix _ ) = Nothing
80{-# INLINE getPort #-}
81
82instance Address a => Address (NodeAddr a) where
83 toSockAddr NodeAddr {..} = setPort nodePort $ toSockAddr nodeHost
84 fromSockAddr sa = NodeAddr <$> fromSockAddr sa <*> getPort sa
85
86instance Address a => Address (PeerAddr a) where
87 toSockAddr PeerAddr {..} = setPort peerPort $ toSockAddr peerHost
88 fromSockAddr sa = PeerAddr Nothing <$> fromSockAddr sa <*> getPort sa
diff --git a/src/Network/BitTorrent/Core/Fingerprint.hs b/src/Network/BitTorrent/Core/Fingerprint.hs
deleted file mode 100644
index d743acd0..00000000
--- a/src/Network/BitTorrent/Core/Fingerprint.hs
+++ /dev/null
@@ -1,290 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- 'Fingerprint' is used to identify the client implementation and
9-- version which also contained in 'Peer'. For exsample first 6
10-- bytes of peer id of this this library are @-HS0100-@ while for
11-- mainline we have @M4-3-6--@. We could extract this info and
12-- print in human-friendly form: this is useful for debugging and
13-- logging.
14--
15-- For more information see:
16-- <http://bittorrent.org/beps/bep_0020.html>
17--
18--
19-- NOTE: Do /not/ use this information to control client
20-- capabilities (such as supported enchancements), this should be
21-- done using 'Network.BitTorrent.Extension'!
22--
23{-# OPTIONS -fno-warn-orphans #-}
24module Network.BitTorrent.Core.Fingerprint
25 ( ClientImpl (..)
26 , Fingerprint (..)
27 , libFingerprint
28 , libUserAgent
29 ) where
30
31import Data.Default
32import Data.List as L
33import Data.List.Split as L
34import Data.Monoid
35import Data.String
36import Data.Version
37import Text.PrettyPrint hiding ((<>))
38import Text.PrettyPrint.Class
39import Text.Read (readMaybe)
40-- import Paths_bittorrent (version)
41
42-- TODO FIXME
43version :: Version
44version = Version [0, 0, 0, 3] []
45
46-- | List of registered client versions + 'IlibHSbittorrent' (this
47-- package) + 'IUnknown' (for not recognized software). All names are
48-- prefixed by \"I\" because some of them starts from lowercase letter
49-- but that is not a valid Haskell constructor name.
50--
51data ClientImpl =
52 IUnknown
53
54 | IMainline
55
56 | IABC
57 | IOspreyPermaseed
58 | IBTQueue
59 | ITribler
60 | IShadow
61 | IBitTornado
62
63-- UPnP(!) Bit Torrent !???
64-- 'U' - UPnP NAT Bit Torrent
65 | IBitLord
66 | IOpera
67 | IMLdonkey
68
69 | IAres
70 | IArctic
71 | IAvicora
72 | IBitPump
73 | IAzureus
74 | IBitBuddy
75 | IBitComet
76 | IBitflu
77 | IBTG
78 | IBitRocket
79 | IBTSlave
80 | IBittorrentX
81 | IEnhancedCTorrent
82 | ICTorrent
83 | IDelugeTorrent
84 | IPropagateDataClient
85 | IEBit
86 | IElectricSheep
87 | IFoxTorrent
88 | IGSTorrent
89 | IHalite
90 | IlibHSbittorrent
91 | IHydranode
92 | IKGet
93 | IKTorrent
94 | ILH_ABC
95 | ILphant
96 | ILibtorrent
97 | ILibTorrent
98 | ILimeWire
99 | IMonoTorrent
100 | IMooPolice
101 | IMiro
102 | IMoonlightTorrent
103 | INetTransport
104 | IPando
105 | IqBittorrent
106 | IQQDownload
107 | IQt4TorrentExample
108 | IRetriever
109 | IShareaza
110 | ISwiftbit
111 | ISwarmScope
112 | ISymTorrent
113 | Isharktorrent
114 | ITorrentDotNET
115 | ITransmission
116 | ITorrentstorm
117 | ITuoTu
118 | IuLeecher
119 | IuTorrent
120 | IVagaa
121 | IBitLet
122 | IFireTorrent
123 | IXunlei
124 | IXanTorrent
125 | IXtorrent
126 | IZipTorrent
127 deriving (Show, Eq, Ord, Enum, Bounded)
128
129-- | Used to represent a not recognized implementation
130instance Default ClientImpl where
131 def = IUnknown
132 {-# INLINE def #-}
133
134-- | Example: @\"BitLet\" == 'IBitLet'@
135instance IsString ClientImpl where
136 fromString str
137 | Just impl <- L.lookup str alist = impl
138 | otherwise = error $ "fromString: not recognized " ++ str
139 where
140 alist = L.map mk [minBound..maxBound]
141 mk x = (L.tail $ show x, x)
142
143-- | Example: @pretty 'IBitLet' == \"IBitLet\"@
144instance Pretty ClientImpl where
145 pretty = text . L.tail . show
146
147-- | Just the '0' version.
148instance Default Version where
149 def = Version [0] []
150 {-# INLINE def #-}
151
152-- | For dot delimited version strings.
153-- Example: @fromString \"0.1.0.2\" == Version [0, 1, 0, 2]@
154--
155instance IsString Version where
156 fromString str
157 | Just nums <- chunkNums str = Version nums []
158 | otherwise = error $ "fromString: invalid version string " ++ str
159 where
160 chunkNums = sequence . L.map readMaybe . L.linesBy ('.' ==)
161
162instance Pretty Version where
163 pretty = text . showVersion
164
165-- | The all sensible infomation that can be obtained from a peer
166-- identifier or torrent /createdBy/ field.
167data Fingerprint = Fingerprint
168 { ciImpl :: ClientImpl
169 , ciVersion :: Version
170 } deriving (Show, Eq, Ord)
171
172-- | Unrecognized client implementation.
173instance Default Fingerprint where
174 def = Fingerprint def def
175 {-# INLINE def #-}
176
177-- | Example: @\"BitComet-1.2\" == ClientInfo IBitComet (Version [1, 2] [])@
178instance IsString Fingerprint where
179 fromString str
180 | _ : ver <- _ver = Fingerprint (fromString impl) (fromString ver)
181 | otherwise = error $ "fromString: invalid client info string" ++ str
182 where
183 (impl, _ver) = L.span ((/=) '-') str
184
185instance Pretty Fingerprint where
186 pretty Fingerprint {..} = pretty ciImpl <+> "version" <+> pretty ciVersion
187
188-- | Fingerprint of this (the bittorrent library) package. Normally,
189-- applications should introduce its own fingerprints, otherwise they
190-- can use 'libFingerprint' value.
191--
192libFingerprint :: Fingerprint
193libFingerprint = Fingerprint IlibHSbittorrent version
194
195-- | HTTP user agent of this (the bittorrent library) package. Can be
196-- used in HTTP tracker requests.
197libUserAgent :: String
198libUserAgent = render (pretty IlibHSbittorrent <> "/" <> pretty version)
199
200{-----------------------------------------------------------------------
201-- For torrent file
202-----------------------------------------------------------------------}
203-- TODO collect information about createdBy torrent field
204{-
205renderImpl :: ClientImpl -> Text
206renderImpl = T.pack . L.tail . show
207
208renderVersion :: Version -> Text
209renderVersion = undefined
210
211renderClientInfo :: ClientInfo -> Text
212renderClientInfo ClientInfo {..} = renderImpl ciImpl <> "/" <> renderVersion ciVersion
213
214parseClientInfo :: Text -> ClientImpl
215parseClientInfo t = undefined
216-}
217{-
218-- code used for generation; remove it later on
219
220mkEnumTyDef :: NM -> String
221mkEnumTyDef = unlines . map (" | I" ++) . nub . map snd
222
223mkPars :: NM -> String
224mkPars = unlines . map (\(code, impl) -> " f \"" ++ code ++ "\" = " ++ "I" ++ impl)
225
226type NM = [(String, String)]
227nameMap :: NM
228nameMap =
229 [ ("AG", "Ares")
230 , ("A~", "Ares")
231 , ("AR", "Arctic")
232 , ("AV", "Avicora")
233 , ("AX", "BitPump")
234 , ("AZ", "Azureus")
235 , ("BB", "BitBuddy")
236 , ("BC", "BitComet")
237 , ("BF", "Bitflu")
238 , ("BG", "BTG")
239 , ("BR", "BitRocket")
240 , ("BS", "BTSlave")
241 , ("BX", "BittorrentX")
242 , ("CD", "EnhancedCTorrent")
243 , ("CT", "CTorrent")
244 , ("DE", "DelugeTorrent")
245 , ("DP", "PropagateDataClient")
246 , ("EB", "EBit")
247 , ("ES", "ElectricSheep")
248 , ("FT", "FoxTorrent")
249 , ("GS", "GSTorrent")
250 , ("HL", "Halite")
251 , ("HS", "libHSnetwork_bittorrent")
252 , ("HN", "Hydranode")
253 , ("KG", "KGet")
254 , ("KT", "KTorrent")
255 , ("LH", "LH_ABC")
256 , ("LP", "Lphant")
257 , ("LT", "Libtorrent")
258 , ("lt", "LibTorrent")
259 , ("LW", "LimeWire")
260 , ("MO", "MonoTorrent")
261 , ("MP", "MooPolice")
262 , ("MR", "Miro")
263 , ("MT", "MoonlightTorrent")
264 , ("NX", "NetTransport")
265 , ("PD", "Pando")
266 , ("qB", "qBittorrent")
267 , ("QD", "QQDownload")
268 , ("QT", "Qt4TorrentExample")
269 , ("RT", "Retriever")
270 , ("S~", "Shareaza")
271 , ("SB", "Swiftbit")
272 , ("SS", "SwarmScope")
273 , ("ST", "SymTorrent")
274 , ("st", "sharktorrent")
275 , ("SZ", "Shareaza")
276 , ("TN", "TorrentDotNET")
277 , ("TR", "Transmission")
278 , ("TS", "Torrentstorm")
279 , ("TT", "TuoTu")
280 , ("UL", "uLeecher")
281 , ("UT", "uTorrent")
282 , ("VG", "Vagaa")
283 , ("WT", "BitLet")
284 , ("WY", "FireTorrent")
285 , ("XL", "Xunlei")
286 , ("XT", "XanTorrent")
287 , ("XX", "Xtorrent")
288 , ("ZT", "ZipTorrent")
289 ]
290-}
diff --git a/src/Network/BitTorrent/Core/NodeInfo.hs b/src/Network/BitTorrent/Core/NodeInfo.hs
deleted file mode 100644
index fe17c097..00000000
--- a/src/Network/BitTorrent/Core/NodeInfo.hs
+++ /dev/null
@@ -1,219 +0,0 @@
1-- |
2-- Module : Network.BitTorrent.Core.Node
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-- A \"node\" is a client\/server listening on a UDP port
11-- implementing the distributed hash table protocol. The DHT is
12-- composed of nodes and stores the location of peers. BitTorrent
13-- clients include a DHT node, which is used to contact other nodes
14-- in the DHT to get the location of peers to download from using
15-- the BitTorrent protocol.
16--
17{-# LANGUAGE RecordWildCards #-}
18{-# LANGUAGE FlexibleInstances #-}
19{-# LANGUAGE TemplateHaskell #-}
20{-# LANGUAGE GeneralizedNewtypeDeriving #-}
21{-# LANGUAGE DeriveDataTypeable #-}
22{-# LANGUAGE DeriveFunctor #-}
23module Network.BitTorrent.Core.NodeInfo
24 ( -- * Node ID
25 NodeId
26 , testIdBit
27 , genNodeId
28
29 -- ** Node distance
30 , NodeDistance
31 , distance
32
33 -- * Node address
34 , NodeAddr (..)
35
36 -- * Node info
37 , NodeInfo (..)
38 , rank
39 ) where
40
41import Control.Applicative
42import Data.Bits
43import Data.ByteString as BS
44import Data.ByteString.Char8 as BC
45import Data.ByteString.Base16 as Base16
46import Data.BEncode as BE
47import Data.Default
48import Data.Hashable
49import Data.Foldable
50import Data.IP
51import Data.List as L
52import Data.Monoid
53import Data.Ord
54import Data.Serialize as S
55import Data.String
56import Data.Typeable
57import Data.Word
58import Network
59import System.Entropy
60import Text.PrettyPrint as PP hiding ((<>))
61import Text.PrettyPrint.Class
62
63import Network.BitTorrent.Core.PeerAddr (PeerAddr (..))
64
65{-----------------------------------------------------------------------
66-- Node id
67-----------------------------------------------------------------------}
68-- TODO more compact representation ('ShortByteString's?)
69
70-- | Each node has a globally unique identifier known as the \"node
71-- ID.\"
72--
73-- Normally, /this/ node id should be saved between invocations
74-- of the client software.
75newtype NodeId = NodeId ByteString
76 deriving (Show, Eq, Ord, BEncode, Typeable)
77
78nodeIdSize :: Int
79nodeIdSize = 20
80
81-- | Meaningless node id, for testing purposes only.
82instance Default NodeId where
83 def = NodeId (BS.replicate nodeIdSize 0)
84
85instance Serialize NodeId where
86 get = NodeId <$> getByteString nodeIdSize
87 {-# INLINE get #-}
88 put (NodeId bs) = putByteString bs
89 {-# INLINE put #-}
90
91-- | ASCII encoded.
92instance IsString NodeId where
93 fromString str
94 | L.length str == nodeIdSize = NodeId (fromString str)
95 | otherwise = error "fromString: invalid NodeId length"
96 {-# INLINE fromString #-}
97
98-- | base16 encoded.
99instance Pretty NodeId where
100 pretty (NodeId nid) = PP.text $ BC.unpack $ Base16.encode nid
101
102-- | Test if the nth bit is set.
103testIdBit :: NodeId -> Word -> Bool
104testIdBit (NodeId bs) i
105 | fromIntegral i < nodeIdSize * 8
106 , (q, r) <- quotRem (fromIntegral i) 8
107 = testBit (BS.index bs q) r
108 | otherwise = False
109{-# INLINE testIdBit #-}
110
111-- TODO WARN is the 'system' random suitable for this?
112-- | Generate random NodeID used for the entire session.
113-- Distribution of ID's should be as uniform as possible.
114--
115genNodeId :: IO NodeId
116genNodeId = NodeId <$> getEntropy nodeIdSize
117
118{-----------------------------------------------------------------------
119-- Node distance
120-----------------------------------------------------------------------}
121
122-- | In Kademlia, the distance metric is XOR and the result is
123-- interpreted as an unsigned integer.
124newtype NodeDistance = NodeDistance BS.ByteString
125 deriving (Eq, Ord)
126
127instance Pretty NodeDistance where
128 pretty (NodeDistance bs) = foldMap bitseq $ BS.unpack bs
129 where
130 listBits w = L.map (testBit w) (L.reverse [0..bitSize w - 1])
131 bitseq = foldMap (int . fromEnum) . listBits
132
133-- | distance(A,B) = |A xor B| Smaller values are closer.
134distance :: NodeId -> NodeId -> NodeDistance
135distance (NodeId a) (NodeId b) = NodeDistance (BS.pack (BS.zipWith xor a b))
136
137{-----------------------------------------------------------------------
138-- Node address
139-----------------------------------------------------------------------}
140
141data NodeAddr a = NodeAddr
142 { nodeHost :: !a
143 , nodePort :: {-# UNPACK #-} !PortNumber
144 } deriving (Eq, Typeable, Functor)
145
146instance Show a => Show (NodeAddr a) where
147 showsPrec i NodeAddr {..}
148 = showsPrec i nodeHost <> showString ":" <> showsPrec i nodePort
149
150instance Read (NodeAddr IPv4) where
151 readsPrec i x = [ (fromPeerAddr a, s) | (a, s) <- readsPrec i x ]
152
153-- | @127.0.0.1:6882@
154instance Default (NodeAddr IPv4) where
155 def = "127.0.0.1:6882"
156
157-- | KRPC compatible encoding.
158instance Serialize a => Serialize (NodeAddr a) where
159 get = NodeAddr <$> get <*> get
160 {-# INLINE get #-}
161 put NodeAddr {..} = put nodeHost >> put nodePort
162 {-# INLINE put #-}
163
164-- | Torrent file compatible encoding.
165instance BEncode a => BEncode (NodeAddr a) where
166 toBEncode NodeAddr {..} = toBEncode (nodeHost, nodePort)
167 {-# INLINE toBEncode #-}
168 fromBEncode b = uncurry NodeAddr <$> fromBEncode b
169 {-# INLINE fromBEncode #-}
170
171instance Hashable a => Hashable (NodeAddr a) where
172 hashWithSalt s NodeAddr {..} = hashWithSalt s (nodeHost, nodePort)
173 {-# INLINE hashWithSalt #-}
174
175instance Pretty ip => Pretty (NodeAddr ip) where
176 pretty NodeAddr {..} = pretty nodeHost <> ":" <> pretty nodePort
177
178-- | Example:
179--
180-- @nodePort \"127.0.0.1:6881\" == 6881@
181--
182instance IsString (NodeAddr IPv4) where
183 fromString = fromPeerAddr . fromString
184
185fromPeerAddr :: PeerAddr a -> NodeAddr a
186fromPeerAddr PeerAddr {..} = NodeAddr
187 { nodeHost = peerHost
188 , nodePort = peerPort
189 }
190
191{-----------------------------------------------------------------------
192-- Node info
193-----------------------------------------------------------------------}
194
195data NodeInfo a = NodeInfo
196 { nodeId :: !NodeId
197 , nodeAddr :: !(NodeAddr a)
198 } deriving (Show, Eq, Functor)
199
200instance Eq a => Ord (NodeInfo a) where
201 compare = comparing nodeId
202
203-- | KRPC 'compact list' compatible encoding: contact information for
204-- nodes is encoded as a 26-byte string. Also known as "Compact node
205-- info" the 20-byte Node ID in network byte order has the compact
206-- IP-address/port info concatenated to the end.
207instance Serialize a => Serialize (NodeInfo a) where
208 get = NodeInfo <$> get <*> get
209 put NodeInfo {..} = put nodeId >> put nodeAddr
210
211instance Pretty ip => Pretty (NodeInfo ip) where
212 pretty NodeInfo {..} = pretty nodeId <> "@(" <> pretty nodeAddr <> ")"
213
214instance Pretty ip => Pretty [NodeInfo ip] where
215 pretty = PP.vcat . PP.punctuate "," . L.map pretty
216
217-- | Order by closeness: nearest nodes first.
218rank :: Eq ip => NodeId -> [NodeInfo ip] -> [NodeInfo ip]
219rank nid = L.sortBy (comparing (distance nid . nodeId))
diff --git a/src/Network/BitTorrent/Core/PeerAddr.hs b/src/Network/BitTorrent/Core/PeerAddr.hs
deleted file mode 100644
index 92fb83a7..00000000
--- a/src/Network/BitTorrent/Core/PeerAddr.hs
+++ /dev/null
@@ -1,354 +0,0 @@
1-- |
2-- Module : Network.BitTorrent.Core.PeerAddr
3-- Copyright : (c) Sam Truzjan 2013
4-- (c) Daniel Gröber 2013
5-- License : BSD3
6-- Maintainer : pxqr.sta@gmail.com
7-- Stability : provisional
8-- Portability : portable
9--
10-- 'PeerAddr' is used to represent peer address. Currently it's
11-- just peer IP and peer port but this might change in future.
12--
13{-# LANGUAGE TemplateHaskell #-}
14{-# LANGUAGE StandaloneDeriving #-}
15{-# LANGUAGE GeneralizedNewtypeDeriving #-}
16{-# LANGUAGE DeriveDataTypeable #-}
17{-# LANGUAGE FlexibleInstances #-}
18{-# LANGUAGE DeriveFunctor #-}
19{-# LANGUAGE ViewPatterns #-}
20{-# OPTIONS -fno-warn-orphans #-} -- for PortNumber instances
21module Network.BitTorrent.Core.PeerAddr
22 ( -- * Peer address
23 PeerAddr(..)
24 , defaultPorts
25 , peerSockAddr
26 , peerSocket
27
28 -- * Peer storage
29 , PeerStore
30 , Network.BitTorrent.Core.PeerAddr.lookup
31 , Network.BitTorrent.Core.PeerAddr.insert
32 ) where
33
34import Control.Applicative
35import Control.Monad
36import Data.BEncode as BS
37import Data.BEncode.BDict (BKey)
38import Data.ByteString.Char8 as BS8
39import Data.Char
40import Data.Default
41import Data.Hashable
42import Data.HashMap.Strict as HM
43import Data.IP
44import Data.List as L
45import Data.List.Split
46import Data.Maybe
47import Data.Monoid
48import Data.Serialize as S
49import Data.String
50import Data.Typeable
51import Data.Word
52import Network.Socket
53import Text.PrettyPrint as PP hiding ((<>))
54import Text.PrettyPrint.Class
55import Text.Read (readMaybe)
56import qualified Text.ParserCombinators.ReadP as RP
57
58import Data.Torrent.InfoHash
59import Network.BitTorrent.Core.PeerId
60
61
62{-----------------------------------------------------------------------
63-- Port number
64-----------------------------------------------------------------------}
65
66instance BEncode PortNumber where
67 toBEncode = toBEncode . fromEnum
68 fromBEncode = fromBEncode >=> portNumber
69 where
70 portNumber :: Integer -> BS.Result PortNumber
71 portNumber n
72 | 0 <= n && n <= fromIntegral (maxBound :: Word16)
73 = pure $ fromIntegral n
74 | otherwise = decodingError $ "PortNumber: " ++ show n
75
76instance Serialize PortNumber where
77 get = fromIntegral <$> getWord16be
78 {-# INLINE get #-}
79 put = putWord16be . fromIntegral
80 {-# INLINE put #-}
81
82instance Hashable PortNumber where
83 hashWithSalt s = hashWithSalt s . fromEnum
84 {-# INLINE hashWithSalt #-}
85
86instance Pretty PortNumber where
87 pretty = PP.int . fromEnum
88 {-# INLINE pretty #-}
89
90{-----------------------------------------------------------------------
91-- IP addr
92-----------------------------------------------------------------------}
93
94class IPAddress i where
95 toHostAddr :: i -> Either HostAddress HostAddress6
96
97instance IPAddress IPv4 where
98 toHostAddr = Left . toHostAddress
99 {-# INLINE toHostAddr #-}
100
101instance IPAddress IPv6 where
102 toHostAddr = Right . toHostAddress6
103 {-# INLINE toHostAddr #-}
104
105instance IPAddress IP where
106 toHostAddr (IPv4 ip) = toHostAddr ip
107 toHostAddr (IPv6 ip) = toHostAddr ip
108 {-# INLINE toHostAddr #-}
109
110deriving instance Typeable IP
111deriving instance Typeable IPv4
112deriving instance Typeable IPv6
113
114ipToBEncode :: Show i => i -> BValue
115ipToBEncode ip = BString $ BS8.pack $ show ip
116{-# INLINE ipToBEncode #-}
117
118ipFromBEncode :: Read a => BValue -> BS.Result a
119ipFromBEncode (BString (BS8.unpack -> ipStr))
120 | Just ip <- readMaybe (ipStr) = pure ip
121 | otherwise = decodingError $ "IP: " ++ ipStr
122ipFromBEncode _ = decodingError $ "IP: addr should be a bstring"
123
124instance BEncode IP where
125 toBEncode = ipToBEncode
126 {-# INLINE toBEncode #-}
127 fromBEncode = ipFromBEncode
128 {-# INLINE fromBEncode #-}
129
130instance BEncode IPv4 where
131 toBEncode = ipToBEncode
132 {-# INLINE toBEncode #-}
133 fromBEncode = ipFromBEncode
134 {-# INLINE fromBEncode #-}
135
136instance BEncode IPv6 where
137 toBEncode = ipToBEncode
138 {-# INLINE toBEncode #-}
139 fromBEncode = ipFromBEncode
140 {-# INLINE fromBEncode #-}
141
142-- | When 'get'ing an IP it must be 'isolate'd to the appropriate
143-- number of bytes since we have no other way of telling which
144-- address type we are trying to parse
145instance Serialize IP where
146 put (IPv4 ip) = put ip
147 put (IPv6 ip) = put ip
148
149 get = do
150 n <- remaining
151 case n of
152 4 -> IPv4 <$> get
153 16 -> IPv6 <$> get
154 _ -> fail "Wrong number of bytes remaining to parse IP"
155
156instance Serialize IPv4 where
157 put = putWord32host . toHostAddress
158 get = fromHostAddress <$> getWord32host
159
160instance Serialize IPv6 where
161 put ip = put $ toHostAddress6 ip
162 get = fromHostAddress6 <$> get
163
164instance Pretty IPv4 where
165 pretty = PP.text . show
166 {-# INLINE pretty #-}
167
168instance Pretty IPv6 where
169 pretty = PP.text . show
170 {-# INLINE pretty #-}
171
172instance Pretty IP where
173 pretty = PP.text . show
174 {-# INLINE pretty #-}
175
176instance Hashable IPv4 where
177 hashWithSalt = hashUsing toHostAddress
178 {-# INLINE hashWithSalt #-}
179
180instance Hashable IPv6 where
181 hashWithSalt s a = hashWithSalt s (toHostAddress6 a)
182
183instance Hashable IP where
184 hashWithSalt s (IPv4 h) = hashWithSalt s h
185 hashWithSalt s (IPv6 h) = hashWithSalt s h
186
187{-----------------------------------------------------------------------
188-- Peer addr
189-----------------------------------------------------------------------}
190-- TODO check semantic of ord and eq instances
191
192-- | Peer address info normally extracted from peer list or peer
193-- compact list encoding.
194data PeerAddr a = PeerAddr
195 { peerId :: !(Maybe PeerId)
196
197 -- | This is usually 'IPv4', 'IPv6', 'IP' or unresolved
198 -- 'HostName'.
199 , peerHost :: !a
200
201 -- | The port the peer listenning for incoming P2P sessions.
202 , peerPort :: {-# UNPACK #-} !PortNumber
203 } deriving (Show, Eq, Ord, Typeable, Functor)
204
205peer_ip_key, peer_id_key, peer_port_key :: BKey
206peer_ip_key = "ip"
207peer_id_key = "peer id"
208peer_port_key = "port"
209
210-- | The tracker's 'announce response' compatible encoding.
211instance (Typeable a, BEncode a) => BEncode (PeerAddr a) where
212 toBEncode PeerAddr {..} = toDict $
213 peer_ip_key .=! peerHost
214 .: peer_id_key .=? peerId
215 .: peer_port_key .=! peerPort
216 .: endDict
217
218 fromBEncode = fromDict $ do
219 peerAddr <$>! peer_ip_key
220 <*>? peer_id_key
221 <*>! peer_port_key
222 where
223 peerAddr = flip PeerAddr
224
225-- | The tracker's 'compact peer list' compatible encoding. The
226-- 'peerId' is always 'Nothing'.
227--
228-- For more info see: <http://www.bittorrent.org/beps/bep_0023.html>
229--
230-- TODO: test byte order
231instance (Serialize a) => Serialize (PeerAddr a) where
232 put PeerAddr {..} = put peerHost >> put peerPort
233 get = PeerAddr Nothing <$> get <*> get
234
235-- | @127.0.0.1:6881@
236instance Default (PeerAddr IPv4) where
237 def = "127.0.0.1:6881"
238
239-- | @127.0.0.1:6881@
240instance Default (PeerAddr IP) where
241 def = IPv4 <$> def
242
243-- | Example:
244--
245-- @peerPort \"127.0.0.1:6881\" == 6881@
246--
247instance IsString (PeerAddr IPv4) where
248 fromString str
249 | [hostAddrStr, portStr] <- splitWhen (== ':') str
250 , Just hostAddr <- readMaybe hostAddrStr
251 , Just portNum <- toEnum <$> readMaybe portStr
252 = PeerAddr Nothing hostAddr portNum
253 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv4): " ++ str
254
255instance Read (PeerAddr IPv4) where
256 readsPrec i = RP.readP_to_S $ do
257 ipv4 <- RP.readS_to_P (readsPrec i)
258 _ <- RP.char ':'
259 port <- toEnum <$> RP.readS_to_P (readsPrec i)
260 return $ PeerAddr Nothing ipv4 port
261
262readsIPv6_port :: String -> [((IPv6, PortNumber), String)]
263readsIPv6_port = RP.readP_to_S $ do
264 ip <- RP.char '[' *> (RP.readS_to_P reads) <* RP.char ']'
265 _ <- RP.char ':'
266 port <- toEnum <$> read <$> (RP.many1 $ RP.satisfy isDigit) <* RP.eof
267 return (ip,port)
268
269instance IsString (PeerAddr IPv6) where
270 fromString str
271 | [((ip,port),"")] <- readsIPv6_port str =
272 PeerAddr Nothing ip port
273 | otherwise = error $ "fromString: unable to parse (PeerAddr IPv6): " ++ str
274
275instance IsString (PeerAddr IP) where
276 fromString str
277 | '[' `L.elem` str = IPv6 <$> fromString str
278 | otherwise = IPv4 <$> fromString str
279
280-- | fingerprint + "at" + dotted.host.inet.addr:port
281-- TODO: instances for IPv6, HostName
282instance Pretty a => Pretty (PeerAddr a) where
283 pretty PeerAddr {..}
284 | Just pid <- peerId = pretty (fingerprint pid) <+> "at" <+> paddr
285 | otherwise = paddr
286 where
287 paddr = pretty peerHost <> ":" <> text (show peerPort)
288
289instance Hashable a => Hashable (PeerAddr a) where
290 hashWithSalt s PeerAddr {..} =
291 s `hashWithSalt` peerId `hashWithSalt` peerHost `hashWithSalt` peerPort
292
293-- | Ports typically reserved for bittorrent P2P listener.
294defaultPorts :: [PortNumber]
295defaultPorts = [6881..6889]
296
297_resolvePeerAddr :: (IPAddress i) => PeerAddr HostName -> PeerAddr i
298_resolvePeerAddr = undefined
299
300_peerSockAddr :: PeerAddr IP -> (Family, SockAddr)
301_peerSockAddr PeerAddr {..} =
302 case peerHost of
303 IPv4 ipv4 ->
304 (AF_INET, SockAddrInet peerPort (toHostAddress ipv4))
305 IPv6 ipv6 ->
306 (AF_INET6, SockAddrInet6 peerPort 0 (toHostAddress6 ipv6) 0)
307
308peerSockAddr :: PeerAddr IP -> SockAddr
309peerSockAddr = snd . _peerSockAddr
310
311-- | Create a socket connected to the address specified in a peerAddr
312peerSocket :: SocketType -> PeerAddr IP -> IO Socket
313peerSocket socketType pa = do
314 let (family, addr) = _peerSockAddr pa
315 sock <- socket family socketType defaultProtocol
316 connect sock addr
317 return sock
318
319{-----------------------------------------------------------------------
320-- Peer storage
321-----------------------------------------------------------------------}
322-- TODO use more memory efficient representation
323
324-- | Storage used to keep track a set of known peers in client,
325-- tracker or DHT sessions.
326newtype PeerStore ip = PeerStore (HashMap InfoHash [PeerAddr ip])
327
328-- | Empty store.
329instance Default (PeerStore a) where
330 def = PeerStore HM.empty
331 {-# INLINE def #-}
332
333-- | Monoid under union operation.
334instance Eq a => Monoid (PeerStore a) where
335 mempty = def
336 {-# INLINE mempty #-}
337
338 mappend (PeerStore a) (PeerStore b) =
339 PeerStore (HM.unionWith L.union a b)
340 {-# INLINE mappend #-}
341
342-- | Can be used to store peers between invocations of the client
343-- software.
344instance Serialize (PeerStore a) where
345 get = undefined
346 put = undefined
347
348-- | Used in 'get_peers' DHT queries.
349lookup :: InfoHash -> PeerStore a -> [PeerAddr a]
350lookup ih (PeerStore m) = fromMaybe [] $ HM.lookup ih m
351
352-- | Used in 'announce_peer' DHT queries.
353insert :: Eq a => InfoHash -> PeerAddr a -> PeerStore a -> PeerStore a
354insert ih a (PeerStore m) = PeerStore (HM.insertWith L.union ih [a] m)
diff --git a/src/Network/BitTorrent/Core/PeerId.hs b/src/Network/BitTorrent/Core/PeerId.hs
deleted file mode 100644
index a180ff30..00000000
--- a/src/Network/BitTorrent/Core/PeerId.hs
+++ /dev/null
@@ -1,364 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- 'PeerID' represent self assigned peer identificator. Ideally each
9-- host in the network should have unique peer id to avoid
10-- collisions, therefore for peer ID generation we use good entropy
11-- source. Peer ID is sent in /tracker request/, sent and received in
12-- /peer handshakes/ and used in DHT queries.
13--
14{-# LANGUAGE GeneralizedNewtypeDeriving #-}
15{-# LANGUAGE MultiParamTypeClasses #-}
16{-# LANGUAGE DeriveDataTypeable #-}
17module Network.BitTorrent.Core.PeerId
18 ( -- * PeerId
19 PeerId
20
21 -- * Generation
22 , genPeerId
23 , timestamp
24 , entropy
25
26 -- * Encoding
27 , azureusStyle
28 , shadowStyle
29 , defaultClientId
30 , defaultVersionNumber
31
32 -- * Decoding
33 , fingerprint
34 ) where
35
36import Control.Applicative
37import Data.BEncode as BE
38import Data.ByteString as BS
39import Data.ByteString.Internal as BS
40import Data.ByteString.Char8 as BC
41import qualified Data.ByteString.Lazy as BL
42import qualified Data.ByteString.Lazy.Builder as BS
43import Data.Convertible
44import Data.Default
45import Data.Foldable (foldMap)
46import Data.List as L
47import Data.List.Split as L
48import Data.Maybe (fromMaybe, catMaybes)
49import Data.Monoid
50import Data.Hashable
51import Data.Serialize as S
52import Data.String
53import Data.Time.Clock (getCurrentTime)
54import Data.Time.Format (formatTime)
55import Data.Typeable
56import Data.Version (Version(Version), versionBranch)
57import Network.HTTP.Types.QueryLike
58import System.Entropy (getEntropy)
59import System.Locale (defaultTimeLocale)
60import Text.PrettyPrint hiding ((<>))
61import Text.PrettyPrint.Class
62import Text.Read (readMaybe)
63
64import Network.BitTorrent.Core.Fingerprint
65
66-- TODO use unpacked Word160 form (length is known statically)
67
68-- | Peer identifier is exactly 20 bytes long bytestring.
69newtype PeerId = PeerId { getPeerId :: ByteString }
70 deriving (Show, Eq, Ord, BEncode, Typeable)
71
72peerIdLen :: Int
73peerIdLen = 20
74
75-- | For testing purposes only.
76instance Default PeerId where
77 def = azureusStyle defaultClientId defaultVersionNumber ""
78
79instance Hashable PeerId where
80 hashWithSalt = hashUsing getPeerId
81 {-# INLINE hashWithSalt #-}
82
83instance Serialize PeerId where
84 put = putByteString . getPeerId
85 get = PeerId <$> getBytes peerIdLen
86
87instance QueryValueLike PeerId where
88 toQueryValue (PeerId pid) = Just pid
89 {-# INLINE toQueryValue #-}
90
91instance IsString PeerId where
92 fromString str
93 | BS.length bs == peerIdLen = PeerId bs
94 | otherwise = error $ "Peer id should be 20 bytes long: " ++ show str
95 where
96 bs = fromString str
97
98instance Pretty PeerId where
99 pretty = text . BC.unpack . getPeerId
100
101instance Convertible BS.ByteString PeerId where
102 safeConvert bs
103 | BS.length bs == peerIdLen = pure (PeerId bs)
104 | otherwise = convError "invalid length" bs
105
106{-----------------------------------------------------------------------
107-- Encoding
108-----------------------------------------------------------------------}
109
110-- | Pad bytestring so it's becomes exactly request length. Conversion
111-- is done like so:
112--
113-- * length < size: Complete bytestring by given charaters.
114--
115-- * length = size: Output bytestring as is.
116--
117-- * length > size: Drop last (length - size) charaters from a
118-- given bytestring.
119--
120byteStringPadded :: ByteString -- ^ bytestring to be padded.
121 -> Int -- ^ size of result builder.
122 -> Char -- ^ character used for padding.
123 -> BS.Builder
124byteStringPadded bs s c =
125 BS.byteString (BS.take s bs) <>
126 BS.byteString (BC.replicate padLen c)
127 where
128 padLen = s - min (BS.length bs) s
129
130-- | Azureus-style encoding have the following layout:
131--
132-- * 1 byte : '-'
133--
134-- * 2 bytes: client id
135--
136-- * 4 bytes: version number
137--
138-- * 1 byte : '-'
139--
140-- * 12 bytes: random number
141--
142azureusStyle :: ByteString -- ^ 2 character client ID, padded with 'H'.
143 -> ByteString -- ^ Version number, padded with 'X'.
144 -> ByteString -- ^ Random number, padded with '0'.
145 -> PeerId -- ^ Azureus-style encoded peer ID.
146azureusStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
147 BS.char8 '-' <>
148 byteStringPadded cid 2 'H' <>
149 byteStringPadded ver 4 'X' <>
150 BS.char8 '-' <>
151 byteStringPadded rnd 12 '0'
152
153-- | Shadow-style encoding have the following layout:
154--
155-- * 1 byte : client id.
156--
157-- * 0-4 bytes: version number. If less than 4 then padded with
158-- '-' char.
159--
160-- * 15 bytes : random number. If length is less than 15 then
161-- padded with '0' char.
162--
163shadowStyle :: Char -- ^ Client ID.
164 -> ByteString -- ^ Version number.
165 -> ByteString -- ^ Random number.
166 -> PeerId -- ^ Shadow style encoded peer ID.
167shadowStyle cid ver rnd = PeerId $ BL.toStrict $ BS.toLazyByteString $
168 BS.char8 cid <>
169 byteStringPadded ver 4 '-' <>
170 byteStringPadded rnd 15 '0'
171
172
173-- | 'HS'- 2 bytes long client identifier.
174defaultClientId :: ByteString
175defaultClientId = "HS"
176
177-- | Gives exactly 4 bytes long version number for any version of the
178-- package. Version is taken from .cabal file.
179defaultVersionNumber :: ByteString
180defaultVersionNumber = BS.take 4 $ BC.pack $ foldMap show $
181 versionBranch $ ciVersion libFingerprint
182
183{-----------------------------------------------------------------------
184-- Generation
185-----------------------------------------------------------------------}
186
187-- | Gives 15 characters long decimal timestamp such that:
188--
189-- * 6 bytes : first 6 characters from picoseconds obtained with %q.
190--
191-- * 1 byte : character \'.\' for readability.
192--
193-- * 9..* bytes: number of whole seconds since the Unix epoch
194-- (!)REVERSED.
195--
196-- Can be used both with shadow and azureus style encoding. This
197-- format is used to make the ID's readable for debugging purposes.
198--
199timestamp :: IO ByteString
200timestamp = (BC.pack . format) <$> getCurrentTime
201 where
202 format t = L.take 6 (formatTime defaultTimeLocale "%q" t) ++ "." ++
203 L.take 9 (L.reverse (formatTime defaultTimeLocale "%s" t))
204
205-- | Gives 15 character long random bytestring. This is more robust
206-- method for generation of random part of peer ID than 'timestamp'.
207entropy :: IO ByteString
208entropy = getEntropy 15
209
210-- NOTE: entropy generates incorrrect peer id
211
212-- | Here we use 'azureusStyle' encoding with the following args:
213--
214-- * 'HS' for the client id; ('defaultClientId')
215--
216-- * Version of the package for the version number;
217-- ('defaultVersionNumber')
218--
219-- * UTC time day ++ day time for the random number. ('timestamp')
220--
221genPeerId :: IO PeerId
222genPeerId = azureusStyle defaultClientId defaultVersionNumber <$> timestamp
223
224{-----------------------------------------------------------------------
225-- Decoding
226-----------------------------------------------------------------------}
227
228parseImpl :: ByteString -> ClientImpl
229parseImpl = f . BC.unpack
230 where
231 f "AG" = IAres
232 f "A~" = IAres
233 f "AR" = IArctic
234 f "AV" = IAvicora
235 f "AX" = IBitPump
236 f "AZ" = IAzureus
237 f "BB" = IBitBuddy
238 f "BC" = IBitComet
239 f "BF" = IBitflu
240 f "BG" = IBTG
241 f "BR" = IBitRocket
242 f "BS" = IBTSlave
243 f "BX" = IBittorrentX
244 f "CD" = IEnhancedCTorrent
245 f "CT" = ICTorrent
246 f "DE" = IDelugeTorrent
247 f "DP" = IPropagateDataClient
248 f "EB" = IEBit
249 f "ES" = IElectricSheep
250 f "FT" = IFoxTorrent
251 f "GS" = IGSTorrent
252 f "HL" = IHalite
253 f "HS" = IlibHSbittorrent
254 f "HN" = IHydranode
255 f "KG" = IKGet
256 f "KT" = IKTorrent
257 f "LH" = ILH_ABC
258 f "LP" = ILphant
259 f "LT" = ILibtorrent
260 f "lt" = ILibTorrent
261 f "LW" = ILimeWire
262 f "MO" = IMonoTorrent
263 f "MP" = IMooPolice
264 f "MR" = IMiro
265 f "ML" = IMLdonkey
266 f "MT" = IMoonlightTorrent
267 f "NX" = INetTransport
268 f "PD" = IPando
269 f "qB" = IqBittorrent
270 f "QD" = IQQDownload
271 f "QT" = IQt4TorrentExample
272 f "RT" = IRetriever
273 f "S~" = IShareaza
274 f "SB" = ISwiftbit
275 f "SS" = ISwarmScope
276 f "ST" = ISymTorrent
277 f "st" = Isharktorrent
278 f "SZ" = IShareaza
279 f "TN" = ITorrentDotNET
280 f "TR" = ITransmission
281 f "TS" = ITorrentstorm
282 f "TT" = ITuoTu
283 f "UL" = IuLeecher
284 f "UT" = IuTorrent
285 f "VG" = IVagaa
286 f "WT" = IBitLet
287 f "WY" = IFireTorrent
288 f "XL" = IXunlei
289 f "XT" = IXanTorrent
290 f "XX" = IXtorrent
291 f "ZT" = IZipTorrent
292 f _ = IUnknown
293
294-- TODO use regexps
295
296-- | Tries to extract meaningful information from peer ID bytes. If
297-- peer id uses unknown coding style then client info returned is
298-- 'def'.
299--
300fingerprint :: PeerId -> Fingerprint
301fingerprint pid = either (const def) id $ runGet getCI (getPeerId pid)
302 where
303 getCI = do
304 leading <- BS.w2c <$> getWord8
305 case leading of
306 '-' -> Fingerprint <$> getAzureusImpl <*> getAzureusVersion
307 'M' -> Fingerprint <$> pure IMainline <*> getMainlineVersion
308 'e' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
309 'F' -> Fingerprint <$> getBitCometImpl <*> getBitCometVersion
310 c -> do
311 c1 <- w2c <$> S.lookAhead getWord8
312 if c1 == 'P'
313 then do
314 _ <- getWord8
315 Fingerprint <$> pure IOpera <*> getOperaVersion
316 else Fingerprint <$> pure (getShadowImpl c) <*> getShadowVersion
317
318 getMainlineVersion = do
319 str <- BC.unpack <$> getByteString 7
320 let mnums = L.filter (not . L.null) $ L.linesBy ('-' ==) str
321 return $ Version (fromMaybe [] $ sequence $ L.map readMaybe mnums) []
322
323 getAzureusImpl = parseImpl <$> getByteString 2
324 getAzureusVersion = mkVer <$> getByteString 4
325 where
326 mkVer bs = Version [fromMaybe 0 $ readMaybe $ BC.unpack bs] []
327
328 getBitCometImpl = do
329 bs <- getByteString 3
330 S.lookAhead $ do
331 _ <- getByteString 2
332 lr <- getByteString 4
333 return $
334 if lr == "LORD" then IBitLord else
335 if bs == "UTB" then IBitComet else
336 if bs == "xbc" then IBitComet else def
337
338 getBitCometVersion = do
339 x <- getWord8
340 y <- getWord8
341 return $ Version [fromIntegral x, fromIntegral y] []
342
343 getOperaVersion = do
344 str <- BC.unpack <$> getByteString 4
345 return $ Version [fromMaybe 0 $ readMaybe str] []
346
347 getShadowImpl 'A' = IABC
348 getShadowImpl 'O' = IOspreyPermaseed
349 getShadowImpl 'Q' = IBTQueue
350 getShadowImpl 'R' = ITribler
351 getShadowImpl 'S' = IShadow
352 getShadowImpl 'T' = IBitTornado
353 getShadowImpl _ = IUnknown
354
355 decodeShadowVerNr :: Char -> Maybe Int
356 decodeShadowVerNr c
357 | '0' < c && c <= '9' = Just (fromEnum c - fromEnum '0')
358 | 'A' < c && c <= 'Z' = Just ((fromEnum c - fromEnum 'A') + 10)
359 | 'a' < c && c <= 'z' = Just ((fromEnum c - fromEnum 'a') + 36)
360 | otherwise = Nothing
361
362 getShadowVersion = do
363 str <- BC.unpack <$> getByteString 5
364 return $ Version (catMaybes $ L.map decodeShadowVerNr str) []
diff --git a/src/Network/BitTorrent/DHT.hs b/src/Network/BitTorrent/DHT.hs
index f587f7c8..39b33478 100644
--- a/src/Network/BitTorrent/DHT.hs
+++ b/src/Network/BitTorrent/DHT.hs
@@ -62,9 +62,8 @@ import Data.Conduit as C
62import Data.Conduit.List as C 62import Data.Conduit.List as C
63import Network.Socket 63import Network.Socket
64 64
65import Data.Torrent (tNodes) 65import Data.Torrent
66import Data.Torrent.InfoHash 66import Network.BitTorrent.Address
67import Network.BitTorrent.Core
68import Network.BitTorrent.DHT.Query 67import Network.BitTorrent.DHT.Query
69import Network.BitTorrent.DHT.Session 68import Network.BitTorrent.DHT.Session
70import Network.BitTorrent.DHT.Routing as T 69import Network.BitTorrent.DHT.Routing as T
diff --git a/src/Network/BitTorrent/DHT/ContactInfo.hs b/src/Network/BitTorrent/DHT/ContactInfo.hs
index 028a4214..baa240b4 100644
--- a/src/Network/BitTorrent/DHT/ContactInfo.hs
+++ b/src/Network/BitTorrent/DHT/ContactInfo.hs
@@ -1,10 +1,24 @@
1module Network.BitTorrent.DHT.ContactInfo 1module Network.BitTorrent.DHT.ContactInfo
2 ( ) where 2 ( PeerStore
3 , Network.BitTorrent.DHT.ContactInfo.lookup
4 , Network.BitTorrent.DHT.ContactInfo.insert
5 ) where
6
7import Data.Default
8import Data.List as L
9import Data.Maybe
10import Data.Monoid
11import Data.HashMap.Strict as HM
12import Data.Serialize
13
14import Data.Torrent
15import Network.BitTorrent.Address
16
3{- 17{-
4import Data.HashMap.Strict as HM 18import Data.HashMap.Strict as HM
5 19
6import Data.Torrent.InfoHash 20import Data.Torrent.InfoHash
7import Network.BitTorrent.Core 21import Network.BitTorrent.Address
8 22
9-- increase prefix when table is too large 23-- increase prefix when table is too large
10-- decrease prefix when table is too small 24-- decrease prefix when table is too small
@@ -90,4 +104,36 @@ prune pref targetSize (Tip _ _) = undefined
90-- | Remove expired entries. 104-- | Remove expired entries.
91splitGT :: Timestamp -> ContactInfo ip -> ContactInfo ip 105splitGT :: Timestamp -> ContactInfo ip -> ContactInfo ip
92splitGT = undefined 106splitGT = undefined
93-} \ No newline at end of file 107-}
108
109-- | Storage used to keep track a set of known peers in client,
110-- tracker or DHT sessions.
111newtype PeerStore ip = PeerStore (HashMap InfoHash [PeerAddr ip])
112
113-- | Empty store.
114instance Default (PeerStore a) where
115 def = PeerStore HM.empty
116 {-# INLINE def #-}
117
118-- | Monoid under union operation.
119instance Eq a => Monoid (PeerStore a) where
120 mempty = def
121 {-# INLINE mempty #-}
122
123 mappend (PeerStore a) (PeerStore b) =
124 PeerStore (HM.unionWith L.union a b)
125 {-# INLINE mappend #-}
126
127-- | Can be used to store peers between invocations of the client
128-- software.
129instance Serialize (PeerStore a) where
130 get = undefined
131 put = undefined
132
133-- | Used in 'get_peers' DHT queries.
134lookup :: InfoHash -> PeerStore a -> [PeerAddr a]
135lookup ih (PeerStore m) = fromMaybe [] $ HM.lookup ih m
136
137-- | Used in 'announce_peer' DHT queries.
138insert :: Eq a => InfoHash -> PeerAddr a -> PeerStore a -> PeerStore a
139insert ih a (PeerStore m) = PeerStore (HM.insertWith L.union ih [a] m)
diff --git a/src/Network/BitTorrent/DHT/Message.hs b/src/Network/BitTorrent/DHT/Message.hs
index 7bcd00f0..145141ee 100644
--- a/src/Network/BitTorrent/DHT/Message.hs
+++ b/src/Network/BitTorrent/DHT/Message.hs
@@ -92,8 +92,8 @@ import Data.Typeable
92import Network 92import Network
93import Network.KRPC 93import Network.KRPC
94 94
95import Data.Torrent.InfoHash 95import Data.Torrent
96import Network.BitTorrent.Core 96import Network.BitTorrent.Address
97import Network.BitTorrent.DHT.Token 97import Network.BitTorrent.DHT.Token
98import Network.KRPC () 98import Network.KRPC ()
99 99
diff --git a/src/Network/BitTorrent/DHT/Query.hs b/src/Network/BitTorrent/DHT/Query.hs
index 48dfc15a..d4710ecf 100644
--- a/src/Network/BitTorrent/DHT/Query.hs
+++ b/src/Network/BitTorrent/DHT/Query.hs
@@ -56,8 +56,8 @@ import Text.PrettyPrint as PP hiding ((<>), ($$))
56import Text.PrettyPrint.Class 56import Text.PrettyPrint.Class
57 57
58import Network.KRPC hiding (Options, def) 58import Network.KRPC hiding (Options, def)
59import Data.Torrent.InfoHash 59import Data.Torrent
60import Network.BitTorrent.Core 60import Network.BitTorrent.Address
61import Network.BitTorrent.DHT.Message 61import Network.BitTorrent.DHT.Message
62import Network.BitTorrent.DHT.Routing 62import Network.BitTorrent.DHT.Routing
63import Network.BitTorrent.DHT.Session 63import Network.BitTorrent.DHT.Session
diff --git a/src/Network/BitTorrent/DHT/Routing.hs b/src/Network/BitTorrent/DHT/Routing.hs
index 106aec31..ee295125 100644
--- a/src/Network/BitTorrent/DHT/Routing.hs
+++ b/src/Network/BitTorrent/DHT/Routing.hs
@@ -73,8 +73,8 @@ import GHC.Generics
73import Text.PrettyPrint as PP hiding ((<>)) 73import Text.PrettyPrint as PP hiding ((<>))
74import Text.PrettyPrint.Class 74import Text.PrettyPrint.Class
75 75
76import Data.Torrent.InfoHash 76import Data.Torrent
77import Network.BitTorrent.Core 77import Network.BitTorrent.Address
78 78
79{----------------------------------------------------------------------- 79{-----------------------------------------------------------------------
80-- Routing monad 80-- Routing monad
diff --git a/src/Network/BitTorrent/DHT/Session.hs b/src/Network/BitTorrent/DHT/Session.hs
index e770b1d3..208f8ec8 100644
--- a/src/Network/BitTorrent/DHT/Session.hs
+++ b/src/Network/BitTorrent/DHT/Session.hs
@@ -75,6 +75,7 @@ import Control.Monad.Logger
75import Control.Monad.Reader 75import Control.Monad.Reader
76import Control.Monad.Trans.Control 76import Control.Monad.Trans.Control
77import Control.Monad.Trans.Resource 77import Control.Monad.Trans.Resource
78import Data.Conduit.Lazy
78import Data.Default 79import Data.Default
79import Data.Fixed 80import Data.Fixed
80import Data.Hashable 81import Data.Hashable
@@ -91,11 +92,11 @@ import System.Random (randomIO)
91import Text.PrettyPrint as PP hiding ((<>), ($$)) 92import Text.PrettyPrint as PP hiding ((<>), ($$))
92import Text.PrettyPrint.Class 93import Text.PrettyPrint.Class
93 94
94import Data.Torrent.InfoHash 95import Data.Torrent as Torrent
95import Network.KRPC hiding (Options, def) 96import Network.KRPC hiding (Options, def)
96import qualified Network.KRPC as KRPC (def) 97import qualified Network.KRPC as KRPC (def)
97import Network.BitTorrent.Core 98import Network.BitTorrent.Address
98import Network.BitTorrent.Core.PeerAddr as P 99import Network.BitTorrent.DHT.ContactInfo as P
99import Network.BitTorrent.DHT.Message 100import Network.BitTorrent.DHT.Message
100import Network.BitTorrent.DHT.Routing as R 101import Network.BitTorrent.DHT.Routing as R
101import Network.BitTorrent.DHT.Token as T 102import Network.BitTorrent.DHT.Token as T
@@ -253,10 +254,8 @@ data Node ip = Node
253-- | DHT keep track current session and proper resource allocation for 254-- | DHT keep track current session and proper resource allocation for
254-- safe multithreading. 255-- safe multithreading.
255newtype DHT ip a = DHT { unDHT :: ReaderT (Node ip) IO a } 256newtype DHT ip a = DHT { unDHT :: ReaderT (Node ip) IO a }
256 deriving ( Functor, Applicative, Monad 257 deriving ( Functor, Applicative, Monad, MonadIO
257 , MonadIO, MonadBase IO 258 , MonadBase IO, MonadReader (Node ip), MonadThrow
258 , MonadReader (Node ip)
259 , MonadThrow, MonadUnsafeIO
260 ) 259 )
261 260
262instance MonadBaseControl IO (DHT ip) where 261instance MonadBaseControl IO (DHT ip) where
diff --git a/src/Network/BitTorrent/DHT/Token.hs b/src/Network/BitTorrent/DHT/Token.hs
index a38456fd..a0ed428b 100644
--- a/src/Network/BitTorrent/DHT/Token.hs
+++ b/src/Network/BitTorrent/DHT/Token.hs
@@ -50,7 +50,7 @@ import Data.String
50import Data.Time 50import Data.Time
51import System.Random 51import System.Random
52 52
53import Network.BitTorrent.Core 53import Network.BitTorrent.Address
54 54
55-- TODO use ShortByteString 55-- TODO use ShortByteString
56 56
diff --git a/src/Network/BitTorrent/Exchange/Assembler.hs b/src/Network/BitTorrent/Exchange/Assembler.hs
deleted file mode 100644
index e5834948..00000000
--- a/src/Network/BitTorrent/Exchange/Assembler.hs
+++ /dev/null
@@ -1,168 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Assembler is used to build pieces from blocks. In general
9-- 'Assembler' should be used to handle 'Transfer' messages when
10--
11-- A block can have one of the following status:
12--
13-- 1) /not allowed/: Piece is not in download set. 'null' and 'empty'.
14--
15--
16-- 2) /waiting/: (allowed?) Block have been allowed to download,
17-- but /this/ peer did not send any 'Request' message for this
18-- block. To allow some piece use
19-- 'Network.BitTorrent.Exchange.Selector' and then 'allowedSet'
20-- and 'allowPiece'.
21--
22-- 3) /inflight/: (pending?) Block have been requested but
23-- /remote/ peer did not send any 'Piece' message for this block.
24-- Related functions 'markInflight'
25--
26-- 4) /pending/: (stalled?) Block have have been downloaded
27-- Related functions 'insertBlock'.
28--
29-- Piece status:
30--
31-- 1) /assembled/: (downloaded?) All blocks in piece have been
32-- downloaded but the piece did not verified yet.
33--
34-- * Valid: go to completed;
35--
36-- * Invalid: go to waiting.
37--
38-- 2) /corrupted/:
39--
40-- 3) /downloaded/: (verified?) A piece have been successfully
41-- verified via the hash. Usually the piece should be stored to
42-- the 'System.Torrent.Storage' and /this/ peer should send 'Have'
43-- messages to the /remote/ peers.
44--
45{-# LANGUAGE TemplateHaskell #-}
46module Network.BitTorrent.Exchange.Assembler
47 ( -- * Assembler
48 Assembler
49
50 -- * Query
51 , Network.BitTorrent.Exchange.Assembler.null
52 , Network.BitTorrent.Exchange.Assembler.size
53
54 -- *
55 , Network.BitTorrent.Exchange.Assembler.empty
56 , allowPiece
57
58 -- * Debugging
59 , Network.BitTorrent.Exchange.Assembler.valid
60 ) where
61
62import Control.Applicative
63import Control.Lens
64import Data.IntMap.Strict as IM
65import Data.List as L
66import Data.Map as M
67import Data.Maybe
68import Data.IP
69
70import Data.Torrent.Piece
71import Network.BitTorrent.Core
72import Network.BitTorrent.Exchange.Block as B
73
74{-----------------------------------------------------------------------
75-- Assembler
76-----------------------------------------------------------------------}
77
78type Timestamp = ()
79{-
80data BlockRequest = BlockRequest
81 { requestSent :: Timestamp
82 , requestedPeer :: PeerAddr IP
83 , requestedBlock :: BlockIx
84 }
85-}
86type BlockRange = (BlockOffset, BlockSize)
87type PieceMap = IntMap
88
89data Assembler = Assembler
90 { -- | A set of blocks that have been 'Request'ed but not yet acked.
91 _inflight :: Map (PeerAddr IP) (PieceMap [BlockRange])
92
93 -- | A set of blocks that but not yet assembled.
94 , _pending :: PieceMap Bucket
95
96 -- | Used for validation of assembled pieces.
97 , info :: PieceInfo
98 }
99
100$(makeLenses ''Assembler)
101
102
103valid :: Assembler -> Bool
104valid = undefined
105
106data Result a
107 = Completed (Piece a)
108 | Corrupted PieceIx
109 | NotRequested PieceIx
110 | Overlapped BlockIx
111
112null :: Assembler -> Bool
113null = undefined
114
115size :: Assembler -> Bool
116size = undefined
117
118empty :: PieceInfo -> Assembler
119empty = Assembler M.empty IM.empty
120
121allowPiece :: PieceIx -> Assembler -> Assembler
122allowPiece pix a @ Assembler {..} = over pending (IM.insert pix bkt) a
123 where
124 bkt = B.empty (piPieceLength info)
125
126allowedSet :: (PeerAddr IP) -> Assembler -> [BlockIx]
127allowedSet = undefined
128
129--inflight :: PeerAddr -> BlockIx -> Assembler -> Assembler
130--inflight = undefined
131
132-- You should check if a returned by peer block is actually have
133-- been requested and in-flight. This is needed to avoid "I send
134-- random corrupted block" attacks.
135insert :: PeerAddr IP -> Block a -> Assembler -> Assembler
136insert = undefined
137
138{-
139insert :: Block a -> Assembler a -> (Assembler a, Maybe (Result a))
140insert blk @ Block {..} a @ Assembler {..} = undefined
141{-
142 = let (pending, mpiece) = inserta blk piecePending
143 in (Assembler inflightSet pending pieceInfo, f <$> mpiece)
144 where
145 f p = undefined
146-- | checkPieceLazy pieceInfo p = Assembled p
147-- | otherwise = Corrupted ixPiece
148-}
149
150
151inflightPieces :: Assembler a -> [PieceIx]
152inflightPieces Assembler {..} = IM.keys piecePending
153
154completeBlocks :: PieceIx -> Assembler a -> [Block a]
155completeBlocks pix Assembler {..} = fromMaybe [] $ IM.lookup pix piecePending
156
157incompleteBlocks :: PieceIx -> Assembler a -> [BlockIx]
158incompleteBlocks = undefined
159
160nextBlock :: Assembler a -> Maybe (Assembler a, BlockIx)
161nextBlock Assembler {..} = undefined
162
163inserta :: Block a
164 -> PieceMap [Block a]
165 -> (PieceMap [Block a], Maybe (Piece a))
166inserta = undefined
167
168-}
diff --git a/src/Data/Torrent/Bitfield.hs b/src/Network/BitTorrent/Exchange/Bitfield.hs
index b65f058b..eca11d83 100644
--- a/src/Data/Torrent/Bitfield.hs
+++ b/src/Network/BitTorrent/Exchange/Bitfield.hs
@@ -27,7 +27,7 @@
27{-# LANGUAGE CPP #-} 27{-# LANGUAGE CPP #-}
28{-# LANGUAGE BangPatterns #-} 28{-# LANGUAGE BangPatterns #-}
29{-# LANGUAGE RecordWildCards #-} 29{-# LANGUAGE RecordWildCards #-}
30module Data.Torrent.Bitfield 30module Network.BitTorrent.Exchange.Bitfield
31 ( -- * Bitfield 31 ( -- * Bitfield
32 PieceIx 32 PieceIx
33 , PieceCount 33 , PieceCount
@@ -43,8 +43,8 @@ module Data.Torrent.Bitfield
43 43
44 -- * Query 44 -- * Query
45 -- ** Cardinality 45 -- ** Cardinality
46 , Data.Torrent.Bitfield.null 46 , Network.BitTorrent.Exchange.Bitfield.null
47 , Data.Torrent.Bitfield.full 47 , Network.BitTorrent.Exchange.Bitfield.full
48 , haveCount 48 , haveCount
49 , totalCount 49 , totalCount
50 , completeness 50 , completeness
@@ -75,6 +75,17 @@ module Data.Torrent.Bitfield
75 -- * Serialization 75 -- * Serialization
76 , fromBitmap 76 , fromBitmap
77 , toBitmap 77 , toBitmap
78
79 -- * Piece selection
80 , Selector
81 , selector
82 , strategyClass
83
84 , strictFirst
85 , strictLast
86 , rarestFirst
87 , randomFirst
88 , endGame
78 ) where 89 ) where
79 90
80import Control.Monad 91import Control.Monad
@@ -92,7 +103,7 @@ import Data.List (foldl')
92import Data.Monoid 103import Data.Monoid
93import Data.Ratio 104import Data.Ratio
94 105
95import Data.Torrent.Piece 106import Data.Torrent
96 107
97-- TODO cache some operations 108-- TODO cache some operations
98 109
@@ -322,3 +333,66 @@ toBitmap Bitfield {..} = {-# SCC toBitmap #-} Lazy.fromChunks [intsetBM, alignme
322 byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1 333 byteSize = bfSize `div` 8 + if bfSize `mod` 8 == 0 then 0 else 1
323 alignment = B.replicate (byteSize - B.length intsetBM) 0 334 alignment = B.replicate (byteSize - B.length intsetBM) 0
324 intsetBM = S.toByteString bfSet 335 intsetBM = S.toByteString bfSet
336
337{-----------------------------------------------------------------------
338-- Piece selection
339-----------------------------------------------------------------------}
340
341type Selector = Bitfield -- ^ Indices of client /have/ pieces.
342 -> Bitfield -- ^ Indices of peer /have/ pieces.
343 -> [Bitfield] -- ^ Indices of other peers /have/ pieces.
344 -> Maybe PieceIx -- ^ Zero-based index of piece to request
345 -- to, if any.
346
347selector :: Selector -- ^ Selector to use at the start.
348 -> Ratio PieceCount
349 -> Selector -- ^ Selector to use after the client have
350 -- the C pieces.
351 -> Selector -- ^ Selector that changes behaviour based
352 -- on completeness.
353selector start pt ready h a xs =
354 case strategyClass pt h of
355 SCBeginning -> start h a xs
356 SCReady -> ready h a xs
357 SCEnd -> endGame h a xs
358
359data StartegyClass
360 = SCBeginning
361 | SCReady
362 | SCEnd
363 deriving (Show, Eq, Ord, Enum, Bounded)
364
365
366strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass
367strategyClass threshold = classify . completeness
368 where
369 classify c
370 | c < threshold = SCBeginning
371 | c + 1 % numerator c < 1 = SCReady
372 -- FIXME numerator have is not total count
373 | otherwise = SCEnd
374
375
376-- | Select the first available piece.
377strictFirst :: Selector
378strictFirst h a _ = Just $ findMin (difference a h)
379
380-- | Select the last available piece.
381strictLast :: Selector
382strictLast h a _ = Just $ findMax (difference a h)
383
384-- |
385rarestFirst :: Selector
386rarestFirst h a xs = rarest (map (intersection want) xs)
387 where
388 want = difference h a
389
390-- | In average random first is faster than rarest first strategy but
391-- only if all pieces are available.
392randomFirst :: Selector
393randomFirst = do
394-- randomIO
395 error "randomFirst"
396
397endGame :: Selector
398endGame = strictLast
diff --git a/src/Network/BitTorrent/Exchange/Block.hs b/src/Network/BitTorrent/Exchange/Block.hs
index 16c124e9..ccc7a0a9 100644
--- a/src/Network/BitTorrent/Exchange/Block.hs
+++ b/src/Network/BitTorrent/Exchange/Block.hs
@@ -69,7 +69,7 @@ import Numeric
69import Text.PrettyPrint as PP hiding ((<>)) 69import Text.PrettyPrint as PP hiding ((<>))
70import Text.PrettyPrint.Class 70import Text.PrettyPrint.Class
71 71
72import Data.Torrent.Piece 72import Data.Torrent
73 73
74{----------------------------------------------------------------------- 74{-----------------------------------------------------------------------
75-- Block attributes 75-- Block attributes
diff --git a/src/Network/BitTorrent/Exchange/Connection.hs b/src/Network/BitTorrent/Exchange/Connection.hs
index fd9022da..2d5f39bf 100644
--- a/src/Network/BitTorrent/Exchange/Connection.hs
+++ b/src/Network/BitTorrent/Exchange/Connection.hs
@@ -112,6 +112,7 @@ import Control.Concurrent hiding (yield)
112import Control.Exception 112import Control.Exception
113import Control.Monad.Reader 113import Control.Monad.Reader
114import Control.Monad.State 114import Control.Monad.State
115import Control.Monad.Trans.Resource
115import Control.Lens 116import Control.Lens
116import Data.ByteString as BS 117import Data.ByteString as BS
117import Data.ByteString.Lazy as BSL 118import Data.ByteString.Lazy as BSL
@@ -135,10 +136,10 @@ import Text.Show.Functions ()
135import System.Log.FastLogger (ToLogStr(..)) 136import System.Log.FastLogger (ToLogStr(..))
136import System.Timeout 137import System.Timeout
137 138
138import Data.Torrent.Bitfield as BF 139import Data.Torrent
139import Data.Torrent.InfoHash 140import Network.BitTorrent.Address
140import Network.BitTorrent.Core 141import Network.BitTorrent.Exchange.Bitfield as BF
141import Network.BitTorrent.Exchange.Message as Msg 142import Network.BitTorrent.Exchange.Message as Msg
142 143
143-- TODO handle port message? 144-- TODO handle port message?
144-- TODO handle limits? 145-- TODO handle limits?
diff --git a/src/Network/BitTorrent/Exchange/Download.hs b/src/Network/BitTorrent/Exchange/Download.hs
new file mode 100644
index 00000000..9a6b5f91
--- /dev/null
+++ b/src/Network/BitTorrent/Exchange/Download.hs
@@ -0,0 +1,295 @@
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 FlexibleInstances #-}
11{-# LANGUAGE MultiParamTypeClasses #-}
12{-# LANGUAGE FunctionalDependencies #-}
13{-# LANGUAGE TemplateHaskell #-}
14module Network.BitTorrent.Exchange.Download
15 ( -- * Downloading
16 Download (..)
17 , Updates
18 , runDownloadUpdates
19
20 -- ** Metadata
21 -- $metadata-download
22 , MetadataDownload
23 , metadataDownload
24
25 -- ** Content
26 -- $content-download
27 , ContentDownload
28 , contentDownload
29 ) where
30
31import Control.Applicative
32import Control.Concurrent
33import Control.Lens
34import Control.Monad.State
35import Data.BEncode as BE
36import Data.ByteString as BS
37import Data.ByteString.Lazy as BL
38import Data.Default
39import Data.List as L
40import Data.Maybe
41import Data.Map as M
42import Data.Tuple
43
44import Data.Torrent as Torrent
45import Network.BitTorrent.Address
46import Network.BitTorrent.Exchange.Bitfield as BF
47import Network.BitTorrent.Exchange.Block as Block
48import Network.BitTorrent.Exchange.Message as Msg
49import System.Torrent.Storage (Storage, writePiece)
50
51
52{-----------------------------------------------------------------------
53-- Class
54-----------------------------------------------------------------------}
55
56type Updates s a = StateT s IO a
57
58runDownloadUpdates :: MVar s -> Updates s a -> IO a
59runDownloadUpdates var m = modifyMVar var (fmap swap . runStateT m)
60
61class Download s chunk | s -> chunk where
62 scheduleBlocks :: Int -> PeerAddr IP -> Bitfield -> Updates s [BlockIx]
63
64 -- |
65 scheduleBlock :: PeerAddr IP -> Bitfield -> Updates s (Maybe BlockIx)
66 scheduleBlock addr bf = listToMaybe <$> scheduleBlocks 1 addr bf
67
68 -- | Get number of sent requests to this peer.
69 getRequestQueueLength :: PeerAddr IP -> Updates s Int
70
71 -- | Remove all pending block requests to the remote peer. May be used
72 -- when:
73 --
74 -- * a peer closes connection;
75 --
76 -- * remote peer choked this peer;
77 --
78 -- * timeout expired.
79 --
80 resetPending :: PeerAddr IP -> Updates s ()
81
82 -- | MAY write to storage, if a new piece have been completed.
83 --
84 -- You should check if a returned by peer block is actually have
85 -- been requested and in-flight. This is needed to avoid "I send
86 -- random corrupted block" attacks.
87 pushBlock :: PeerAddr IP -> chunk -> Updates s (Maybe Bool)
88
89{-----------------------------------------------------------------------
90-- Metadata download
91-----------------------------------------------------------------------}
92-- $metadata-download
93-- TODO
94
95data MetadataDownload = MetadataDownload
96 { _pendingPieces :: [(PeerAddr IP, PieceIx)]
97 , _bucket :: Bucket
98 , _topic :: InfoHash
99 }
100
101makeLenses ''MetadataDownload
102
103-- | Create a new scheduler for infodict of the given size.
104metadataDownload :: Int -> InfoHash -> MetadataDownload
105metadataDownload ps = MetadataDownload [] (Block.empty ps)
106
107instance Default MetadataDownload where
108 def = error "instance Default MetadataDownload"
109
110--cancelPending :: PieceIx -> Updates ()
111cancelPending pix = pendingPieces %= L.filter ((pix ==) . snd)
112
113instance Download MetadataDownload (Piece BS.ByteString) where
114 scheduleBlock addr bf = do
115 bkt <- use bucket
116 case spans metadataPieceSize bkt of
117 [] -> return Nothing
118 ((off, _ ) : _) -> do
119 let pix = off `div` metadataPieceSize
120 pendingPieces %= ((addr, pix) :)
121 return (Just (BlockIx pix 0 metadataPieceSize))
122
123 resetPending addr = pendingPieces %= L.filter ((addr ==) . fst)
124
125 pushBlock addr Torrent.Piece {..} = do
126 p <- use pendingPieces
127 when ((addr, pieceIndex) `L.notElem` p) $
128 error "not requested"
129 cancelPending pieceIndex
130
131 bucket %= Block.insert (metadataPieceSize * pieceIndex) pieceData
132 b <- use bucket
133 case toPiece b of
134 Nothing -> return Nothing
135 Just chunks -> do
136 t <- use topic
137 case parseInfoDict (BL.toStrict chunks) t of
138 Right x -> do
139 pendingPieces .= []
140 return undefined -- (Just x)
141 Left e -> do
142 pendingPieces .= []
143 bucket .= Block.empty (Block.size b)
144 return undefined -- Nothing
145 where
146 -- todo use incremental parsing to avoid BS.concat call
147 parseInfoDict :: BS.ByteString -> InfoHash -> Result InfoDict
148 parseInfoDict chunk topic =
149 case BE.decode chunk of
150 Right (infodict @ InfoDict {..})
151 | topic == idInfoHash -> return infodict
152 | otherwise -> Left "broken infodict"
153 Left err -> Left $ "unable to parse infodict " ++ err
154
155{-----------------------------------------------------------------------
156-- Content download
157-----------------------------------------------------------------------}
158-- $content-download
159--
160-- A block can have one of the following status:
161--
162-- 1) /not allowed/: Piece is not in download set.
163--
164-- 2) /waiting/: (allowed?) Block have been allowed to download,
165-- but /this/ peer did not send any 'Request' message for this
166-- block. To allow some piece use
167-- 'Network.BitTorrent.Exchange.Selector' and then 'allowedSet'
168-- and 'allowPiece'.
169--
170-- 3) /inflight/: (pending?) Block have been requested but
171-- /remote/ peer did not send any 'Piece' message for this block.
172-- Related functions 'markInflight'
173--
174-- 4) /pending/: (stalled?) Block have have been downloaded
175-- Related functions 'insertBlock'.
176--
177-- Piece status:
178--
179-- 1) /assembled/: (downloaded?) All blocks in piece have been
180-- downloaded but the piece did not verified yet.
181--
182-- * Valid: go to completed;
183--
184-- * Invalid: go to waiting.
185--
186-- 2) /corrupted/:
187--
188-- 3) /downloaded/: (verified?) A piece have been successfully
189-- verified via the hash. Usually the piece should be stored to
190-- the 'System.Torrent.Storage' and /this/ peer should send 'Have'
191-- messages to the /remote/ peers.
192--
193
194data PieceEntry = PieceEntry
195 { pending :: [(PeerAddr IP, BlockIx)]
196 , stalled :: Bucket
197 }
198
199pieceEntry :: PieceSize -> PieceEntry
200pieceEntry s = PieceEntry [] (Block.empty s)
201
202isEmpty :: PieceEntry -> Bool
203isEmpty PieceEntry {..} = L.null pending && Block.null stalled
204
205_holes :: PieceIx -> PieceEntry -> [BlockIx]
206_holes pix PieceEntry {..} = fmap mkBlockIx (spans defaultTransferSize stalled)
207 where
208 mkBlockIx (off, sz) = BlockIx pix off sz
209
210data ContentDownload = ContentDownload
211 { inprogress :: !(Map PieceIx PieceEntry)
212 , bitfield :: !Bitfield
213 , pieceSize :: !PieceSize
214 , contentStorage :: Storage
215 }
216
217contentDownload :: Bitfield -> PieceSize -> Storage -> ContentDownload
218contentDownload = ContentDownload M.empty
219
220--modifyEntry :: PieceIx -> (PieceEntry -> PieceEntry) -> DownloadUpdates ()
221modifyEntry pix f = modify $ \ s @ ContentDownload {..} -> s
222 { inprogress = alter (g pieceSize) pix inprogress }
223 where
224 g s = h . f . fromMaybe (pieceEntry s)
225 h e
226 | isEmpty e = Nothing
227 | otherwise = Just e
228
229instance Download ContentDownload (Block BL.ByteString) where
230 scheduleBlocks n addr maskBF = do
231 ContentDownload {..} <- get
232 let wantPieces = maskBF `BF.difference` bitfield
233 let wantBlocks = L.concat $ M.elems $ M.mapWithKey _holes $
234 M.filterWithKey (\ pix _ -> pix `BF.member` wantPieces)
235 inprogress
236
237 bixs <- if L.null wantBlocks
238 then do
239 mpix <- choosePiece wantPieces
240 case mpix of -- TODO return 'n' blocks
241 Nothing -> return []
242 Just pix -> return [leadingBlock pix defaultTransferSize]
243 else chooseBlocks wantBlocks n
244
245 forM_ bixs $ \ bix -> do
246 modifyEntry (ixPiece bix) $ \ e @ PieceEntry {..} -> e
247 { pending = (addr, bix) : pending }
248
249 return bixs
250 where
251 -- TODO choose block nearest to pending or stalled sets to reduce disk
252 -- seeks on remote machines
253 --chooseBlocks :: [BlockIx] -> Int -> DownloadUpdates [BlockIx]
254 chooseBlocks xs n = return (L.take n xs)
255
256 -- TODO use selection strategies from Exchange.Selector
257 --choosePiece :: Bitfield -> DownloadUpdates (Maybe PieceIx)
258 choosePiece bf
259 | BF.null bf = return $ Nothing
260 | otherwise = return $ Just $ BF.findMin bf
261
262 getRequestQueueLength addr = do
263 m <- gets (M.map (L.filter ((==) addr . fst) . pending) . inprogress)
264 return $ L.sum $ L.map L.length $ M.elems m
265
266 resetPending addr = modify $ \ s -> s { inprogress = reset (inprogress s) }
267 where
268 reset = fmap $ \ e -> e
269 { pending = L.filter (not . (==) addr . fst) (pending e) }
270
271 pushBlock addr blk @ Block {..} = do
272 mpe <- gets (M.lookup blkPiece . inprogress)
273 case mpe of
274 Nothing -> return Nothing
275 Just (pe @ PieceEntry {..})
276 | blockIx blk `L.notElem` fmap snd pending -> return Nothing
277 | otherwise -> do
278 let bkt' = Block.insertLazy blkOffset blkData stalled
279 case toPiece bkt' of
280 Nothing -> do
281 modifyEntry blkPiece $ \ e @ PieceEntry {..} -> e
282 { pending = L.filter ((==) (blockIx blk) . snd) pending
283 , stalled = bkt'
284 }
285 return (Just False)
286
287 Just pieceData -> do
288 -- TODO verify
289 storage <- gets contentStorage
290 liftIO $ writePiece (Torrent.Piece blkPiece pieceData) storage
291 modify $ \ s @ ContentDownload {..} -> s
292 { inprogress = M.delete blkPiece inprogress
293 , bitfield = BF.insert blkPiece bitfield
294 }
295 return (Just True)
diff --git a/src/Network/BitTorrent/Exchange/Manager.hs b/src/Network/BitTorrent/Exchange/Manager.hs
index b9aaa818..54727805 100644
--- a/src/Network/BitTorrent/Exchange/Manager.hs
+++ b/src/Network/BitTorrent/Exchange/Manager.hs
@@ -12,8 +12,8 @@ import Control.Monad
12import Data.Default 12import Data.Default
13import Network.Socket 13import Network.Socket
14 14
15import Data.Torrent.InfoHash 15import Data.Torrent
16import Network.BitTorrent.Core 16import Network.BitTorrent.Address
17import Network.BitTorrent.Exchange.Connection hiding (Options) 17import Network.BitTorrent.Exchange.Connection hiding (Options)
18import Network.BitTorrent.Exchange.Session 18import Network.BitTorrent.Exchange.Session
19 19
diff --git a/src/Network/BitTorrent/Exchange/Message.hs b/src/Network/BitTorrent/Exchange/Message.hs
index d8873f95..f8b76186 100644
--- a/src/Network/BitTorrent/Exchange/Message.hs
+++ b/src/Network/BitTorrent/Exchange/Message.hs
@@ -117,10 +117,10 @@ import Network.Socket hiding (KeepAlive)
117import Text.PrettyPrint as PP hiding ((<>)) 117import Text.PrettyPrint as PP hiding ((<>))
118import Text.PrettyPrint.Class 118import Text.PrettyPrint.Class
119 119
120import Data.Torrent.Bitfield 120import Data.Torrent hiding (Piece (..))
121import Data.Torrent.InfoHash 121import qualified Data.Torrent as P (Piece (..))
122import qualified Data.Torrent.Piece as P 122import Network.BitTorrent.Address
123import Network.BitTorrent.Core 123import Network.BitTorrent.Exchange.Bitfield
124import Network.BitTorrent.Exchange.Block 124import Network.BitTorrent.Exchange.Block
125 125
126{----------------------------------------------------------------------- 126{-----------------------------------------------------------------------
@@ -864,7 +864,7 @@ instance PeerMessage ExtendedMetadata where
864 864
865-- | All 'Piece's in 'MetadataData' messages MUST have size equal to 865-- | All 'Piece's in 'MetadataData' messages MUST have size equal to
866-- this value. The last trailing piece can be shorter. 866-- this value. The last trailing piece can be shorter.
867metadataPieceSize :: P.PieceSize 867metadataPieceSize :: PieceSize
868metadataPieceSize = 16 * 1024 868metadataPieceSize = 16 * 1024
869 869
870isLastPiece :: P.Piece a -> Int -> Bool 870isLastPiece :: P.Piece a -> Int -> Bool
@@ -877,8 +877,8 @@ isLastPiece P.Piece {..} total = succ pieceIndex == pcnt
877-- length; otherwise serialization MUST fail. 877-- length; otherwise serialization MUST fail.
878isValidPiece :: P.Piece BL.ByteString -> Int -> Bool 878isValidPiece :: P.Piece BL.ByteString -> Int -> Bool
879isValidPiece p @ P.Piece {..} total 879isValidPiece p @ P.Piece {..} total
880 | isLastPiece p total = P.pieceSize p <= metadataPieceSize 880 | isLastPiece p total = pieceSize p <= metadataPieceSize
881 | otherwise = P.pieceSize p == metadataPieceSize 881 | otherwise = pieceSize p == metadataPieceSize
882 882
883setMetadataPayload :: BS.ByteString -> ExtendedMetadata -> ExtendedMetadata 883setMetadataPayload :: BS.ByteString -> ExtendedMetadata -> ExtendedMetadata
884setMetadataPayload bs (MetadataData (P.Piece pix _) t) = 884setMetadataPayload bs (MetadataData (P.Piece pix _) t) =
diff --git a/src/Network/BitTorrent/Exchange/Selection.hs b/src/Network/BitTorrent/Exchange/Selection.hs
deleted file mode 100644
index 2724fabc..00000000
--- a/src/Network/BitTorrent/Exchange/Selection.hs
+++ /dev/null
@@ -1,85 +0,0 @@
1-- |
2-- Copyright : (c) Sam Truzjan 2013
3-- License : BSD3
4-- Maintainer : pxqr.sta@gmail.com
5-- Stability : experimental
6-- Portability : portable
7--
8-- Piece selection algorithms.
9--
10module Network.BitTorrent.Exchange.Selection
11 ( -- * Selection
12 Selector
13 , selector
14 , strategyClass
15
16 , strictFirst
17 , strictLast
18 , rarestFirst
19 , randomFirst
20 , endGame
21 ) where
22
23import Data.Ratio
24
25import Data.Torrent.Bitfield
26
27
28type Selector = Bitfield -- ^ Indices of client /have/ pieces.
29 -> Bitfield -- ^ Indices of peer /have/ pieces.
30 -> [Bitfield] -- ^ Indices of other peers /have/ pieces.
31 -> Maybe PieceIx -- ^ Zero-based index of piece to request
32 -- to, if any.
33
34selector :: Selector -- ^ Selector to use at the start.
35 -> Ratio PieceCount
36 -> Selector -- ^ Selector to use after the client have
37 -- the C pieces.
38 -> Selector -- ^ Selector that changes behaviour based
39 -- on completeness.
40selector start pt ready h a xs =
41 case strategyClass pt h of
42 SCBeginning -> start h a xs
43 SCReady -> ready h a xs
44 SCEnd -> endGame h a xs
45
46data StartegyClass
47 = SCBeginning
48 | SCReady
49 | SCEnd
50 deriving (Show, Eq, Ord, Enum, Bounded)
51
52
53strategyClass :: Ratio PieceCount -> Bitfield -> StartegyClass
54strategyClass threshold = classify . completeness
55 where
56 classify c
57 | c < threshold = SCBeginning
58 | c + 1 % numerator c < 1 = SCReady
59 -- FIXME numerator have is not total count
60 | otherwise = SCEnd
61
62
63-- | Select the first available piece.
64strictFirst :: Selector
65strictFirst h a _ = Just $ findMin (difference a h)
66
67-- | Select the last available piece.
68strictLast :: Selector
69strictLast h a _ = Just $ findMax (difference a h)
70
71-- |
72rarestFirst :: Selector
73rarestFirst h a xs = rarest (map (intersection want) xs)
74 where
75 want = difference h a
76
77-- | In average random first is faster than rarest first strategy but
78-- only if all pieces are available.
79randomFirst :: Selector
80randomFirst = do
81-- randomIO
82 error "randomFirst"
83
84endGame :: Selector
85endGame = strictLast
diff --git a/src/Network/BitTorrent/Exchange/Session.hs b/src/Network/BitTorrent/Exchange/Session.hs
index 6f480ce4..30b7ed0e 100644
--- a/src/Network/BitTorrent/Exchange/Session.hs
+++ b/src/Network/BitTorrent/Exchange/Session.hs
@@ -45,18 +45,14 @@ import Text.PrettyPrint.Class
45import System.Log.FastLogger (LogStr, ToLogStr (..)) 45import System.Log.FastLogger (LogStr, ToLogStr (..))
46 46
47import Data.BEncode as BE 47import Data.BEncode as BE
48import Data.Torrent (InfoDict (..)) 48import Data.Torrent as Torrent
49import Data.Torrent.Bitfield as BF
50import Data.Torrent.InfoHash
51import Data.Torrent.Piece
52import qualified Data.Torrent.Piece as Torrent (Piece ())
53import Network.BitTorrent.Internal.Types 49import Network.BitTorrent.Internal.Types
54import Network.BitTorrent.Core 50import Network.BitTorrent.Address
51import Network.BitTorrent.Exchange.Bitfield as BF
55import Network.BitTorrent.Exchange.Block as Block 52import Network.BitTorrent.Exchange.Block as Block
56import Network.BitTorrent.Exchange.Connection 53import Network.BitTorrent.Exchange.Connection
54import Network.BitTorrent.Exchange.Download as D
57import Network.BitTorrent.Exchange.Message as Message 55import Network.BitTorrent.Exchange.Message as Message
58import Network.BitTorrent.Exchange.Session.Metadata as Metadata
59import Network.BitTorrent.Exchange.Session.Status as SS
60import System.Torrent.Storage 56import System.Torrent.Storage
61 57
62{----------------------------------------------------------------------- 58{-----------------------------------------------------------------------
@@ -93,13 +89,13 @@ type LogFun = Loc -> LogSource -> LogLevel -> LogStr -> IO ()
93 89
94data SessionState 90data SessionState
95 = WaitingMetadata 91 = WaitingMetadata
96 { metadataDownload :: MVar Metadata.Status 92 { metadataDownload :: MVar MetadataDownload
97 , metadataCompleted :: MVar InfoDict -- ^ used to unblock waiters 93 , metadataCompleted :: MVar InfoDict -- ^ used to unblock waiters
98 , contentRootPath :: FilePath 94 , contentRootPath :: FilePath
99 } 95 }
100 | HavingMetadata 96 | HavingMetadata
101 { metadataCache :: Cached InfoDict 97 { metadataCache :: Cached InfoDict
102 , contentDownload :: MVar SessionStatus 98 , contentDownload :: MVar ContentDownload
103 , contentStorage :: Storage 99 , contentStorage :: Storage
104 } 100 }
105 101
@@ -108,8 +104,9 @@ newSessionState rootPath (Left ih ) = do
108 WaitingMetadata <$> newMVar def <*> newEmptyMVar <*> pure rootPath 104 WaitingMetadata <$> newMVar def <*> newEmptyMVar <*> pure rootPath
109newSessionState rootPath (Right dict) = do 105newSessionState rootPath (Right dict) = do
110 storage <- openInfoDict ReadWriteEx rootPath dict 106 storage <- openInfoDict ReadWriteEx rootPath dict
111 download <- newMVar $ sessionStatus (BF.haveNone (totalPieces storage)) 107 download <- newMVar $ D.contentDownload (BF.haveNone (totalPieces storage))
112 (piPieceLength (idPieceInfo dict)) 108 (piPieceLength (idPieceInfo dict))
109 storage
113 return $ HavingMetadata (cache dict) download storage 110 return $ HavingMetadata (cache dict) download storage
114 111
115closeSessionState :: SessionState -> IO () 112closeSessionState :: SessionState -> IO ()
@@ -119,8 +116,9 @@ closeSessionState HavingMetadata {..} = close contentStorage
119haveMetadata :: InfoDict -> SessionState -> IO SessionState 116haveMetadata :: InfoDict -> SessionState -> IO SessionState
120haveMetadata dict WaitingMetadata {..} = do 117haveMetadata dict WaitingMetadata {..} = do
121 storage <- openInfoDict ReadWriteEx contentRootPath dict 118 storage <- openInfoDict ReadWriteEx contentRootPath dict
122 download <- newMVar $ sessionStatus (BF.haveNone (totalPieces storage)) 119 download <- newMVar $ D.contentDownload (BF.haveNone (totalPieces storage))
123 (piPieceLength (idPieceInfo dict)) 120 (piPieceLength (idPieceInfo dict))
121 storage
124 return HavingMetadata 122 return HavingMetadata
125 { metadataCache = cache dict 123 { metadataCache = cache dict
126 , contentDownload = download 124 , contentDownload = download
diff --git a/src/Network/BitTorrent/Exchange/Session/Metadata.hs b/src/Network/BitTorrent/Exchange/Session/Metadata.hs
deleted file mode 100644
index 79156e2e..00000000
--- a/src/Network/BitTorrent/Exchange/Session/Metadata.hs
+++ /dev/null
@@ -1,104 +0,0 @@
1{-# LANGUAGE TemplateHaskell #-}
2module Network.BitTorrent.Exchange.Session.Metadata
3 ( -- * Transfer state
4 Status
5 , nullStatus
6
7 -- * State updates
8 , Updates
9 , runUpdates
10
11 -- * Piece transfer control
12 , scheduleBlock
13 , resetPending
14 , cancelPending
15 , pushBlock
16 ) where
17
18import Control.Concurrent
19import Control.Lens
20import Control.Monad.Reader
21import Control.Monad.State
22import Data.ByteString as BS
23import Data.ByteString.Lazy as BL
24import Data.Default
25import Data.List as L
26import Data.Tuple
27
28import Data.BEncode as BE
29import Data.Torrent
30import Data.Torrent.InfoHash
31import Data.Torrent.Piece as Torrent
32import Network.BitTorrent.Core
33import Network.BitTorrent.Exchange.Block as Block
34import Network.BitTorrent.Exchange.Message as Message hiding (Status)
35
36
37-- | Current transfer status.
38data Status = Status
39 { _pending :: [(PeerAddr IP, PieceIx)]
40 , _bucket :: Bucket
41 }
42
43makeLenses ''Status
44
45instance Default Status where
46 def = error "default status"
47
48-- | Create a new scheduler for infodict of the given size.
49nullStatus :: Int -> Status
50nullStatus ps = Status [] (Block.empty ps)
51
52type Updates = ReaderT (PeerAddr IP) (State Status)
53
54runUpdates :: MVar Status -> PeerAddr IP -> Updates a -> IO a
55runUpdates v a m = modifyMVar v (return . swap . runState (runReaderT m a))
56
57scheduleBlock :: Updates (Maybe PieceIx)
58scheduleBlock = do
59 addr <- ask
60 bkt <- use bucket
61 case spans metadataPieceSize bkt of
62 [] -> return Nothing
63 ((off, _ ) : _) -> do
64 let pix = off `div` metadataPieceSize
65 pending %= ((addr, pix) :)
66 return (Just pix)
67
68cancelPending :: PieceIx -> Updates ()
69cancelPending pix = pending %= L.filter ((pix ==) . snd)
70
71resetPending :: Updates ()
72resetPending = do
73 addr <- ask
74 pending %= L.filter ((addr ==) . fst)
75
76parseInfoDict :: BS.ByteString -> InfoHash -> Result InfoDict
77parseInfoDict chunk topic =
78 case BE.decode chunk of
79 Right (infodict @ InfoDict {..})
80 | topic == idInfoHash -> return infodict
81 | otherwise -> Left "broken infodict"
82 Left err -> Left $ "unable to parse infodict " ++ err
83
84-- todo use incremental parsing to avoid BS.concat call
85pushBlock :: Torrent.Piece BS.ByteString -> InfoHash -> Updates (Maybe InfoDict)
86pushBlock Torrent.Piece {..} topic = do
87 addr <- ask
88 p <- use pending
89 when ((addr, pieceIndex) `L.notElem` p) $ error "not requested"
90 cancelPending pieceIndex
91
92 bucket %= Block.insert (metadataPieceSize * pieceIndex) pieceData
93 b <- use bucket
94 case toPiece b of
95 Nothing -> return Nothing
96 Just chunks ->
97 case parseInfoDict (BL.toStrict chunks) topic of
98 Right x -> do
99 pending .= []
100 return (Just x)
101 Left e -> do
102 pending .= []
103 bucket .= Block.empty (Block.size b)
104 return Nothing
diff --git a/src/Network/BitTorrent/Exchange/Session/Status.hs b/src/Network/BitTorrent/Exchange/Session/Status.hs
deleted file mode 100644
index 565c3bf3..00000000
--- a/src/Network/BitTorrent/Exchange/Session/Status.hs
+++ /dev/null
@@ -1,175 +0,0 @@
1module Network.BitTorrent.Exchange.Session.Status
2 ( -- * Environment
3 StatusUpdates
4 , runStatusUpdates
5
6 -- * Status
7 , SessionStatus
8 , sessionStatus
9
10 -- * Query
11 , getBitfield
12 , getRequestQueueLength
13
14 -- * Control
15 , scheduleBlocks
16 , resetPending
17 , pushBlock
18 ) where
19
20import Control.Applicative
21import Control.Concurrent
22import Control.Monad.State
23import Data.ByteString.Lazy as BL
24import Data.Default
25import Data.List as L
26import Data.Maybe
27import Data.Map as M
28import Data.Set as S
29import Data.Tuple
30
31import Data.Torrent.Piece
32import Data.Torrent.Bitfield as BF
33import Network.BitTorrent.Core
34import Network.BitTorrent.Exchange.Block as Block
35import System.Torrent.Storage (Storage, writePiece)
36
37
38{-----------------------------------------------------------------------
39-- Piece entry
40-----------------------------------------------------------------------}
41
42data PieceEntry = PieceEntry
43 { pending :: [(PeerAddr IP, BlockIx)]
44 , stalled :: Bucket
45 }
46
47pieceEntry :: PieceSize -> PieceEntry
48pieceEntry s = PieceEntry [] (Block.empty s)
49
50isEmpty :: PieceEntry -> Bool
51isEmpty PieceEntry {..} = L.null pending && Block.null stalled
52
53holes :: PieceIx -> PieceEntry -> [BlockIx]
54holes pix PieceEntry {..} = fmap mkBlockIx (spans defaultTransferSize stalled)
55 where
56 mkBlockIx (off, sz) = BlockIx pix off sz
57
58{-----------------------------------------------------------------------
59-- Session status
60-----------------------------------------------------------------------}
61
62data SessionStatus = SessionStatus
63 { inprogress :: !(Map PieceIx PieceEntry)
64 , bitfield :: !Bitfield
65 , pieceSize :: !PieceSize
66 }
67
68sessionStatus :: Bitfield -> PieceSize -> SessionStatus
69sessionStatus bf ps = SessionStatus
70 { inprogress = M.empty
71 , bitfield = bf
72 , pieceSize = ps
73 }
74
75type StatusUpdates a = StateT SessionStatus IO a
76
77-- |
78runStatusUpdates :: MVar SessionStatus -> StatusUpdates a -> IO a
79runStatusUpdates var m = modifyMVar var (fmap swap . runStateT m)
80
81getBitfield :: MVar SessionStatus -> IO Bitfield
82getBitfield var = bitfield <$> readMVar var
83
84getRequestQueueLength :: PeerAddr IP -> StatusUpdates Int
85getRequestQueueLength addr = do
86 m <- gets (M.elems . M.map (L.filter ((==) addr . fst) . pending) . inprogress)
87 return $ L.sum $ L.map L.length m
88
89modifyEntry :: PieceIx -> (PieceEntry -> PieceEntry) -> StatusUpdates ()
90modifyEntry pix f = modify $ \ s @ SessionStatus {..} -> s
91 { inprogress = alter (g pieceSize) pix inprogress }
92 where
93 g s = h . f . fromMaybe (pieceEntry s)
94 h e
95 | isEmpty e = Nothing
96 | otherwise = Just e
97
98{-----------------------------------------------------------------------
99-- Piece download
100-----------------------------------------------------------------------}
101
102-- TODO choose block nearest to pending or stalled sets to reduce disk
103-- seeks on remote machines
104chooseBlocks :: [BlockIx] -> Int -> StatusUpdates [BlockIx]
105chooseBlocks xs n = return (L.take n xs)
106
107-- TODO use selection strategies from Exchange.Selector
108choosePiece :: Bitfield -> StatusUpdates (Maybe PieceIx)
109choosePiece bf
110 | BF.null bf = return $ Nothing
111 | otherwise = return $ Just $ BF.findMin bf
112
113scheduleBlocks :: PeerAddr IP -> Bitfield -> Int -> StatusUpdates [BlockIx]
114scheduleBlocks addr maskBF n = do
115 SessionStatus {..} <- get
116 let wantPieces = maskBF `BF.difference` bitfield
117 let wantBlocks = L.concat $ M.elems $ M.mapWithKey holes $
118 M.filterWithKey (\ pix _ -> pix `BF.member` wantPieces) inprogress
119
120 bixs <- if L.null wantBlocks
121 then do
122 mpix <- choosePiece wantPieces
123 case mpix of -- TODO return 'n' blocks
124 Nothing -> return []
125 Just pix -> return [leadingBlock pix defaultTransferSize]
126 else chooseBlocks wantBlocks n
127
128 forM_ bixs $ \ bix -> do
129 modifyEntry (ixPiece bix) $ \ e @ PieceEntry {..} -> e
130 { pending = (addr, bix) : pending }
131
132 return bixs
133
134
135-- | Remove all pending block requests to the remote peer. May be used
136-- when:
137--
138-- * a peer closes connection;
139--
140-- * remote peer choked this peer;
141--
142-- * timeout expired.
143--
144resetPending :: PeerAddr IP -> StatusUpdates ()
145resetPending addr = modify $ \ s -> s { inprogress = reset (inprogress s) }
146 where
147 reset = fmap $ \ e -> e
148 { pending = L.filter (not . (==) addr . fst) (pending e) }
149
150-- | MAY write to storage, if a new piece have been completed.
151pushBlock :: Block BL.ByteString -> Storage -> StatusUpdates (Maybe Bool)
152pushBlock blk @ Block {..} storage = do
153 mpe <- gets (M.lookup blkPiece . inprogress)
154 case mpe of
155 Nothing -> return Nothing
156 Just (pe @ PieceEntry {..})
157 | blockIx blk `L.notElem` fmap snd pending -> return Nothing
158 | otherwise -> do
159 let bkt' = Block.insertLazy blkOffset blkData stalled
160 case toPiece bkt' of
161 Nothing -> do
162 modifyEntry blkPiece $ \ e @ PieceEntry {..} -> e
163 { pending = L.filter ((==) (blockIx blk) . snd) pending
164 , stalled = bkt'
165 }
166 return (Just False)
167
168 Just pieceData -> do
169 -- TODO verify
170 liftIO $ writePiece (Piece blkPiece pieceData) storage
171 modify $ \ s @ SessionStatus {..} -> s
172 { inprogress = M.delete blkPiece inprogress
173 , bitfield = BF.insert blkPiece bitfield
174 }
175 return (Just True)
diff --git a/src/Data/Torrent/Progress.hs b/src/Network/BitTorrent/Internal/Progress.hs
index 4719020a..9aff9935 100644
--- a/src/Data/Torrent/Progress.hs
+++ b/src/Network/BitTorrent/Internal/Progress.hs
@@ -13,7 +13,7 @@
13{-# LANGUAGE TemplateHaskell #-} 13{-# LANGUAGE TemplateHaskell #-}
14{-# LANGUAGE ViewPatterns #-} 14{-# LANGUAGE ViewPatterns #-}
15{-# OPTIONS -fno-warn-orphans #-} 15{-# OPTIONS -fno-warn-orphans #-}
16module Data.Torrent.Progress 16module Network.BitTorrent.Internal.Progress
17 ( -- * Progress 17 ( -- * Progress
18 Progress (..) 18 Progress (..)
19 19
@@ -39,7 +39,6 @@ import Control.Lens hiding ((%=))
39import Data.ByteString.Lazy.Builder as BS 39import Data.ByteString.Lazy.Builder as BS
40import Data.ByteString.Lazy.Builder.ASCII as BS 40import Data.ByteString.Lazy.Builder.ASCII as BS
41import Data.Default 41import Data.Default
42import Data.List as L
43import Data.Monoid 42import Data.Monoid
44import Data.Serialize as S 43import Data.Serialize as S
45import Data.Ratio 44import Data.Ratio
diff --git a/src/Network/BitTorrent/Tracker/Message.hs b/src/Network/BitTorrent/Tracker/Message.hs
index cdc07af8..e4a41045 100644
--- a/src/Network/BitTorrent/Tracker/Message.hs
+++ b/src/Network/BitTorrent/Tracker/Message.hs
@@ -124,10 +124,9 @@ import Numeric
124import System.Entropy 124import System.Entropy
125import Text.Read (readMaybe) 125import Text.Read (readMaybe)
126 126
127import Data.Torrent.InfoHash 127import Data.Torrent
128import Data.Torrent.Progress 128import Network.BitTorrent.Address
129import Network.BitTorrent.Core 129import Network.BitTorrent.Internal.Progress
130
131 130
132{----------------------------------------------------------------------- 131{-----------------------------------------------------------------------
133-- Events 132-- Events
diff --git a/src/Network/BitTorrent/Tracker/RPC.hs b/src/Network/BitTorrent/Tracker/RPC.hs
index dc1bd4ec..6fd22b25 100644
--- a/src/Network/BitTorrent/Tracker/RPC.hs
+++ b/src/Network/BitTorrent/Tracker/RPC.hs
@@ -25,7 +25,7 @@ module Network.BitTorrent.Tracker.RPC
25 -- * RPC 25 -- * RPC
26 , SAnnounceQuery (..) 26 , SAnnounceQuery (..)
27 , RpcException (..) 27 , RpcException (..)
28 , announce 28 , Network.BitTorrent.Tracker.RPC.announce
29 , scrape 29 , scrape
30 ) where 30 ) where
31 31
@@ -36,9 +36,9 @@ import Network
36import Network.URI 36import Network.URI
37import Network.Socket (HostAddress) 37import Network.Socket (HostAddress)
38 38
39import Data.Torrent.InfoHash 39import Data.Torrent
40import Data.Torrent.Progress 40import Network.BitTorrent.Address
41import Network.BitTorrent.Core 41import Network.BitTorrent.Internal.Progress
42import Network.BitTorrent.Tracker.Message 42import Network.BitTorrent.Tracker.Message
43import qualified Network.BitTorrent.Tracker.RPC.HTTP as HTTP 43import qualified Network.BitTorrent.Tracker.RPC.HTTP as HTTP
44import qualified Network.BitTorrent.Tracker.RPC.UDP as UDP 44import qualified Network.BitTorrent.Tracker.RPC.UDP as UDP
diff --git a/src/Network/BitTorrent/Tracker/RPC/HTTP.hs b/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
index 4a8e5f79..6e55eb04 100644
--- a/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
+++ b/src/Network/BitTorrent/Tracker/RPC/HTTP.hs
@@ -47,8 +47,8 @@ import qualified Network.HTTP.Conduit as HTTP
47import Network.HTTP.Types.Header (hUserAgent) 47import Network.HTTP.Types.Header (hUserAgent)
48import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery) 48import Network.HTTP.Types.URI (SimpleQuery, renderSimpleQuery)
49 49
50import Data.Torrent.InfoHash (InfoHash) 50import Data.Torrent (InfoHash)
51import Network.BitTorrent.Core.Fingerprint (libUserAgent) 51import Network.BitTorrent.Address (libUserAgent)
52import Network.BitTorrent.Tracker.Message hiding (Request, Response) 52import Network.BitTorrent.Tracker.Message hiding (Request, Response)
53 53
54{----------------------------------------------------------------------- 54{-----------------------------------------------------------------------
diff --git a/src/Network/BitTorrent/Tracker/Session.hs b/src/Network/BitTorrent/Tracker/Session.hs
index 560acf84..cef7d665 100644
--- a/src/Network/BitTorrent/Tracker/Session.hs
+++ b/src/Network/BitTorrent/Tracker/Session.hs
@@ -57,8 +57,8 @@ import Data.Time
57import Data.Traversable 57import Data.Traversable
58import Network.URI 58import Network.URI
59 59
60import Data.Torrent.InfoHash 60import Data.Torrent
61import Network.BitTorrent.Core 61import Network.BitTorrent.Address
62import Network.BitTorrent.Internal.Cache 62import Network.BitTorrent.Internal.Cache
63import Network.BitTorrent.Internal.Types 63import Network.BitTorrent.Internal.Types
64import Network.BitTorrent.Tracker.List as TL 64import Network.BitTorrent.Tracker.List as TL
diff --git a/src/System/Torrent/FileMap.hs b/src/System/Torrent/FileMap.hs
index 80907a30..6e8d7f5a 100644
--- a/src/System/Torrent/FileMap.hs
+++ b/src/System/Torrent/FileMap.hs
@@ -34,7 +34,7 @@ import Data.Vector as V -- TODO use unboxed vector
34import Foreign 34import Foreign
35import System.IO.MMap 35import System.IO.MMap
36 36
37import Data.Torrent.Layout 37import Data.Torrent
38 38
39 39
40data FileEntry = FileEntry 40data FileEntry = FileEntry
diff --git a/src/System/Torrent/Storage.hs b/src/System/Torrent/Storage.hs
index 003a4e98..1d77e55d 100644
--- a/src/System/Torrent/Storage.hs
+++ b/src/System/Torrent/Storage.hs
@@ -56,9 +56,7 @@ import Data.Conduit.List as C
56import Data.Typeable 56import Data.Typeable
57 57
58import Data.Torrent 58import Data.Torrent
59import Data.Torrent.Bitfield as BF 59import Network.BitTorrent.Exchange.Bitfield as BF
60import Data.Torrent.Layout
61import Data.Torrent.Piece
62import System.Torrent.FileMap as FM 60import System.Torrent.FileMap as FM
63 61
64 62
diff --git a/src/Data/Torrent/Tree.hs b/src/System/Torrent/Tree.hs
index 102f4dff..41cfb360 100644
--- a/src/Data/Torrent/Tree.hs
+++ b/src/System/Torrent/Tree.hs
@@ -10,7 +10,7 @@
10{-# LANGUAGE FlexibleInstances #-} 10{-# LANGUAGE FlexibleInstances #-}
11{-# LANGUAGE TemplateHaskell #-} 11{-# LANGUAGE TemplateHaskell #-}
12{-# LANGUAGE DeriveDataTypeable #-} 12{-# LANGUAGE DeriveDataTypeable #-}
13module Data.Torrent.Tree 13module System.Torrent.Tree
14 ( -- * Directory tree 14 ( -- * Directory tree
15 DirTree (..) 15 DirTree (..)
16 16
@@ -18,7 +18,7 @@ module Data.Torrent.Tree
18 , build 18 , build
19 19
20 -- * Query 20 -- * Query
21 , Data.Torrent.Tree.lookup 21 , System.Torrent.Tree.lookup
22 , lookupDir 22 , lookupDir
23 , fileNumber 23 , fileNumber
24 , dirNumber 24 , dirNumber
@@ -31,7 +31,7 @@ import Data.List as L
31import Data.Map as M 31import Data.Map as M
32import Data.Monoid 32import Data.Monoid
33 33
34import Data.Torrent.Layout 34import Data.Torrent
35 35
36 36
37-- | 'DirTree' is more convenient form of 'LayoutInfo'. 37-- | 'DirTree' is more convenient form of 'LayoutInfo'.
@@ -61,13 +61,13 @@ build MultiFile {..} = Dir $ M.singleton liDirName files
61lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a) 61lookup :: [FilePath] -> DirTree a -> Maybe (DirTree a)
62lookup [] t = Just t 62lookup [] t = Just t
63lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m 63lookup (p : ps) (Dir m) | Just subTree <- M.lookup (BC.pack p) m
64 = Data.Torrent.Tree.lookup ps subTree 64 = System.Torrent.Tree.lookup ps subTree
65lookup _ _ = Nothing 65lookup _ _ = Nothing
66 66
67-- | Lookup directory by path. 67-- | Lookup directory by path.
68lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)] 68lookupDir :: [FilePath] -> DirTree a -> Maybe [(ByteString, DirTree a)]
69lookupDir ps d = do 69lookupDir ps d = do
70 subTree <- Data.Torrent.Tree.lookup ps d 70 subTree <- System.Torrent.Tree.lookup ps d
71 case subTree of 71 case subTree of
72 File _ -> Nothing 72 File _ -> Nothing
73 Dir es -> Just $ M.toList es 73 Dir es -> Just $ M.toList es