module FuncTorrent
(BVal(..),
- Info,
- InfoDict,
- Metainfo,
+ Info(..),
+ Metainfo(..),
Peer,
TrackerResponse(..),
- announceList,
connect,
decode,
encode,
handShakeMsg,
- info,
- infoHash,
initLogger,
- lengthInBytes,
logMessage,
logStop,
mkInfo,
mkMetaInfo,
- mkTrackerResponse,
- name,
- prepareRequest,
- urlEncodeHash
+ mkTrackerResponse
) where
import FuncTorrent.Bencode
{-# LANGUAGE OverloadedStrings #-}
-module FuncTorrent.Bencode (
- BVal(..)
- , InfoDict
- , bstrToString
- , bValToInteger
- , bValToInfoDict
+module FuncTorrent.Bencode
+ (BVal(..)
, bValToBList
, bValToBstr
- , encode
+ , bValToInfoDict
+ , bValToInteger
+ , bstrToString
, decode
+ , encode
) where
import Prelude hiding (length, concat)
data BVal = Bint Integer
| Bstr ByteString
| Blist [BVal]
- | Bdict InfoDict
+ | Bdict (Map String BVal)
deriving (Ord, Eq, Show)
-- getters
bValToBList (Blist lst) = Just lst
bValToBList _ = Nothing
-bValToInfoDict :: BVal -> Maybe InfoDict
+bValToInfoDict :: BVal -> Maybe (Map String BVal)
bValToInfoDict (Bdict dict) = Just dict
bValToInfoDict _ = Nothing
bstrToString :: BVal -> Maybe String
bstrToString bval = unpack <$> bValToBstr bval
-type InfoDict = Map String BVal
-
-- $setup
-- >>> import Data.Either
-- Right (fromList [("spam",Blist [Bstr "a",Bstr "b"])])
-- >>> parse bencDict "Bdict" (pack "d9:publisher3:bob17:publisher-webpage15:www.example.com18:publisher.location4:homee")
-- Right (fromList [("publisher",Bstr "bob"),("publisher-webpage",Bstr "www.example.com"),("publisher.location",Bstr "home")])
-bencDict :: ParsecBS.Parser InfoDict
+bencDict :: ParsecBS.Parser (Map String BVal)
bencDict = between (char 'd') (char 'e') $ fromList <$> many kvpair
where kvpair = do k <- bencStr
v <- bencVal
--
-- The MVar in stop is just to ensure the logger thread executes completely
-- Before exiting the main application.
---
data Logger = Logger (MVar LogCommand)
data LogCommand = Message String | Stop (MVar ())
module FuncTorrent.Metainfo
(Info(..),
Metainfo(..),
- infoHash,
mkInfo,
mkMetaInfo
) where
import Crypto.Hash.SHA1 (hash)
import Data.Maybe (maybeToList)
-import FuncTorrent.Bencode (BVal(..), InfoDict, encode, bstrToString, bValToInteger)
+import FuncTorrent.Bencode (BVal(..), encode, bstrToString, bValToInteger)
-- only single file mode supported for the time being.
data Info = Info { pieceLength :: !Integer
, comment :: !(Maybe String)
, createdBy :: !(Maybe String)
, encoding :: !(Maybe String)
+ , infoHash :: !ByteString
} deriving (Eq, Show)
mkInfo :: BVal -> Maybe Info
, comment = bstrToString =<< comment'
, createdBy = bstrToString =<< createdBy'
, encoding = bstrToString =<< encoding'
+ , infoHash = hash . encode $ (m ! "info")
}
+
mkMetaInfo _ = Nothing
getAnnounceList :: Maybe BVal -> [String]
_ -> "") l
getAnnounceList (Just (Bdict _)) = []
-
--- | Info hash is urlencoded 20 byte SHA1 hash of the value of the info key from
--- the Metainfo file. Note that the value will be a bencoded dictionary, given
--- the definition of the info key above. TODO: `Metainfo -> ByteString`
-infoHash :: InfoDict -> ByteString
-infoHash m = hash . encode $ (m ! "info")
import Data.Int (Int8)
import qualified Data.Binary as Bin (encode)
-import FuncTorrent.Bencode (InfoDict)
-import FuncTorrent.Metainfo (infoHash)
+import FuncTorrent.Metainfo (Metainfo(..))
-- | Peer is a IP address, port tuple
data Peer = Peer String Integer
deriving (Show, Eq)
-handShakeMsg :: InfoDict -> String -> ByteString
+handShakeMsg :: Metainfo -> String -> ByteString
handShakeMsg m peer_id = concat [pstrlen, pstr, reserved, infoH, peerID]
where pstrlen = concat $ toChunks $ Bin.encode (19 :: Int8)
pstr = pack "BitTorrent protocol"
module FuncTorrent.Tracker
(TrackerResponse(..),
connect,
+ mkArgs,
+ mkParams,
mkTrackerResponse,
- prepareRequest,
urlEncodeHash
) where
import Prelude hiding (lookup, concat, replicate, splitAt)
-import Data.ByteString.Char8 (ByteString, unpack, splitAt)
+
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 as BC (pack, unpack, splitAt, concat, intercalate)
import Data.Char (chr)
import Data.List (intercalate)
import Data.Map as M (lookup)
-import Data.Maybe (fromJust)
import Network.HTTP (simpleHTTP, defaultGETRequest_, getResponseBody)
import Network.HTTP.Base (urlEncode)
import Network.URI (parseURI)
import qualified Data.ByteString.Base16 as B16 (encode)
-import FuncTorrent.Bencode (BVal(..), InfoDict)
-import FuncTorrent.Metainfo (infoHash)
+import FuncTorrent.Bencode (BVal(..))
import FuncTorrent.Peer (Peer(..))
import FuncTorrent.Utils (splitN)
+import FuncTorrent.Metainfo (Info(..), Metainfo(..))
-- | Tracker response
, incomplete :: Maybe Integer
} deriving (Show, Eq)
-type Url = String
-
-- | Deserialize tracker response
mkTrackerResponse :: BVal -> Either ByteString TrackerResponse
mkTrackerResponse resp =
toPort = read . ("0x" ++) . unpack . B16.encode
toIP :: ByteString -> String
- toIP = intercalate "." .
+ toIP = Data.List.intercalate "." .
map (show . toInt . ("0x" ++) . unpack) .
splitN 2 . B16.encode
makePeer peer = Peer (toIP ip') (toPort port')
where (ip', port') = splitAt 4 peer
+-- | Connect to a tracker and get peer info
+connect :: Metainfo -> String -> IO ByteString
+connect m peer_id = get (head . announceList $ m) $ mkArgs m peer_id
--- | urlEncodeHash
---
--- >>> urlEncodeHash $ pack "123456789abcdef123456789abcdef123456789a"
--- "%124Vx%9a%bc%de%f1%23Eg%89%ab%cd%ef%124Vx%9a"
+--- | URL encode hash as per RFC1738
+--- TODO: Add tests
+--- REVIEW: Why is this not written in terms of `Network.HTTP.Base.urlEncode` or
+--- equivalent library function?
urlEncodeHash :: ByteString -> String
urlEncodeHash bs = concatMap (encode' . unpack) (splitN 2 bs)
where encode' b@[c1, c2] = let c = chr (read ("0x" ++ b))
nonSpecialChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"
-prepareRequest :: InfoDict -> String -> Integer -> String
-prepareRequest d peer_id len =
- let p = [("info_hash", urlEncodeHash ((B16.encode . infoHash) d)),
- ("peer_id", urlEncode peer_id),
- ("port", "6881"),
- ("uploaded", "0"),
- ("downloaded", "0"),
- ("left", show len),
- ("compact", "1"),
- ("event", "started")]
- in intercalate "&" [f ++ "=" ++ s | (f,s) <- p]
-
-connect :: Url -> String -> IO ByteString
-connect baseurl qstr = simpleHTTP (defaultGETRequest_ url) >>= getResponseBody
- where url = fromJust . parseURI $ (baseurl ++ "?" ++ qstr)
+-- | Make arguments that should be posted to tracker.
+-- This is a separate pure function for testability.
+mkArgs :: Metainfo -> String -> [(String, ByteString)]
+mkArgs m peer_id = [("info_hash", pack . urlEncodeHash . B16.encode . infoHash $ m),
+ ("peer_id", pack . urlEncode $ peer_id),
+ ("port", "6881"),
+ ("uploaded", "0"),
+ ("downloaded", "0"),
+ ("left", pack . show . lengthInBytes $ info m),
+ ("compact", "1"),
+ ("event", "started")]
+
+-- | Make a query string from a alist of k, v
+-- TODO: Url encode each argument
+mkParams :: [(String, ByteString)] -> ByteString
+mkParams params = BC.intercalate "&" [concat [pack f, "=", s] | (f,s) <- params]
+
+get :: String -> [(String, ByteString)] -> IO ByteString
+get url args = simpleHTTP (defaultGETRequest_ url') >>= getResponseBody
+ where url' = case parseURI $ unpack $ concat [pack url, "?", qstr] of
+ Just x -> x
+ _ -> error "Bad tracker URL"
+ qstr = mkParams args
{-# LANGUAGE OverloadedStrings #-}
module Main where
-import Prelude hiding (length, readFile, writeFile)
+import Prelude hiding (log, length, readFile, writeFile)
import Data.ByteString.Char8 (ByteString, readFile, writeFile, length, unpack)
import System.Environment (getArgs)
import System.Exit (exitSuccess)
import System.Directory (doesFileExist)
import Text.ParserCombinators.Parsec (ParseError)
-import FuncTorrent.Bencode (decode, BVal(..))
+import FuncTorrent.Bencode (decode)
import FuncTorrent.Logger (initLogger, logMessage, logStop)
import FuncTorrent.Metainfo (Info(..), Metainfo(..), mkMetaInfo)
import FuncTorrent.Peer (handShakeMsg)
-import FuncTorrent.Tracker (connect, prepareRequest, peers, mkTrackerResponse)
+import FuncTorrent.Tracker (connect, peers, mkTrackerResponse)
logError :: ParseError -> (String -> IO ()) -> IO ()
logError e logMsg = logMsg $ "parse error: \n" ++ show e
main = do
args <- getArgs
logR <- initLogger
- let logMsg = logMessage logR
- logMsg $ "Parsing input file: " ++ concat args
+ let log = logMessage logR
+ log "Starting up functorrent"
+ log $ "Parsing input file " ++ concat args
torrentStr <- parse args
case decode torrentStr of
Right d ->
case mkMetaInfo d of
- Nothing -> logMsg "parse error"
+ Nothing -> log "Unable to make meta info file"
Just m -> do
- logMsg "Input File OK"
+ log "Input File OK"
+ log $ "Downloading file : " ++ name (info m)
+ log "Trying to fetch peers"
- let len = lengthInBytes $ info m
- (Bdict d') = d
- trackers = announceList m
+ log $ "Trackers: " ++ head (announceList m)
+ response <- connect m peerId
- logMsg "Trying to fetch peers: "
- response <- connect (head trackers) (prepareRequest d' peerId len)
-
- let hsMsgLen = show $ length $ handShakeMsg d' peerId
- logMsg $ "Hand-shake message length : " ++ hsMsgLen
+ let hsMsgLen = show $ length $ handShakeMsg m peerId
+ log $ "Hand-shake message length : " ++ hsMsgLen
-- TODO: Write to ~/.functorrent/caches
writeFile (name (info m) ++ ".cache") response
Right trackerInfo ->
case mkTrackerResponse trackerInfo of
Right peerResp ->
- logMsg $ "Peers List : " ++ (show . peers $ peerResp)
- Left e -> logMsg $ "Error" ++ unpack e
- Left e -> logError e logMsg
+ log $ "Peers List : " ++ (show . peers $ peerResp)
+ Left e -> log $ "Error" ++ unpack e
+ Left e -> logError e log
- Left e -> logError e logMsg
+ Left e -> logError e log
logStop logR
module Main where
import Prelude hiding (readFile)
-import Data.ByteString.Char8 (ByteString, readFile)
+
+import Data.ByteString (ByteString, readFile)
import Data.Map.Strict (fromList)
import Test.Tasty
import FuncTorrent.Bencode (decode, BVal(..))
import FuncTorrent.Metainfo (Info(..), Metainfo(..), mkMetaInfo)
import FuncTorrent.Peer (Peer(..))
-import FuncTorrent.Tracker (TrackerResponse(..), peers, mkTrackerResponse)
+import FuncTorrent.Tracker
-- Parsed .torrent file
file :: BVal
lengthInBytes = 12,
md5sum = Nothing
},
+ infoHash = "\249\SYN\145=\129\182\205\\\181v0\144\154\EM\150f\152\221]}",
announceList = ["http://9.rarbg.com:2710/announce"],
creationDate = Just 1428717851,
comment = Just "hello world",