summaryrefslogtreecommitdiff
path: root/src/Network/BitTorrent/Client.hs
blob: 8e59d798e9afac671108e6af45555ef9989bd000 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE TypeSynonymInstances       #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell            #-}
module Network.BitTorrent.Client
       ( -- * Options
         Options (..)

         -- * Client session
       , Client

         -- ** Session data
       , clientPeerId
       , clientListenerPort
       , allowedExtensions

         -- ** Session initialization
       , newClient
       , closeClient
       , withClient
       , simpleClient

         -- * BitTorrent monad
       , MonadBitTorrent (..)
       , BitTorrent
       , runBitTorrent
       , getClient

         -- * Handle
       , Handle
       , topic
       , TorrentSource (..)
       , closeHandle
       , getIndex

       , start
       , pause
       , stop
       ) where

import Control.Applicative
import Control.Exception
import Control.Concurrent
import Control.Monad.Logger
import Control.Monad.Trans
import Control.Monad.Trans.Resource

import Data.Default
import Data.HashMap.Strict as HM
import Data.Text
import Network

import           Data.Torrent
import           Data.Torrent.InfoHash
import           Data.Torrent.Magnet
import           Network.BitTorrent.Client.Types
import           Network.BitTorrent.Client.Handle
import           Network.BitTorrent.Core
import           Network.BitTorrent.DHT      as DHT      hiding (Options)
import           Network.BitTorrent.Tracker  as Tracker  hiding (Options)
import           Network.BitTorrent.Exchange as Exchange hiding (Options)
import qualified Network.BitTorrent.Exchange as Exchange (Options(..))


data Options = Options
  { optFingerprint :: Fingerprint
  , optName        :: Text
  , optPort        :: PortNumber
  , optExtensions  :: [Extension]
  , optNodeAddr    :: NodeAddr IPv4
  , optBootNode    :: Maybe (NodeAddr IPv4)
  }

instance Default Options where
  def = Options
    { optFingerprint = def
    , optName        = "hs-bittorrent"
    , optPort        = 6882
    , optExtensions  = []
    , optNodeAddr    = "0.0.0.0:6882"
    , optBootNode    = Nothing
    }

exchangeOptions :: PeerId -> Options -> Exchange.Options
exchangeOptions pid Options {..} = Exchange.Options
  { optPeerAddr = PeerAddr (Just pid) (peerHost def) optPort
  , optBacklog  = optBacklog def
  }

connHandler :: MVar (HashMap InfoHash Handle) -> Exchange.Handler
connHandler tmap ih = do
  m <- readMVar tmap
  case HM.lookup ih m of
    Nothing            -> error "torrent not found"
    Just (Handle {..}) -> return exchange

initClient :: Options -> LogFun -> ResIO Client
initClient opts @ Options {..} logFun = do
  pid  <- liftIO genPeerId
  tmap <- liftIO $ newMVar HM.empty

  let peerInfo = PeerInfo pid Nothing optPort
  let mkTracker = Tracker.newManager def peerInfo
  (_, tmgr) <- allocate mkTracker Tracker.closeManager

  let mkEx = Exchange.newManager (exchangeOptions pid opts) (connHandler tmap)
  (_, emgr) <- allocate mkEx Exchange.closeManager

  let mkNode = DHT.newNode defaultHandlers def optNodeAddr logFun
  (_, node) <- allocate mkNode DHT.closeNode

  resourceMap <- getInternalState
  return Client
    { clientPeerId       = pid
    , clientListenerPort = optPort
    , allowedExtensions  = toCaps optExtensions
    , clientResources    = resourceMap
    , trackerManager     = tmgr
    , exchangeManager    = emgr
    , clientNode         = node
    , clientTorrents     = tmap
    , clientLogger       = logFun
    }

newClient :: Options -> LogFun -> IO Client
newClient opts logFun = do
  s <- createInternalState
  runInternalState (initClient opts logFun) s
    `onException` closeInternalState s

closeClient :: Client -> IO ()
closeClient Client {..} = closeInternalState clientResources

withClient :: Options -> LogFun -> (Client -> IO a) -> IO a
withClient opts lf action = bracket (newClient opts lf) closeClient action

-- do not perform IO in 'initClient', do it in the 'boot'
--boot :: BitTorrent ()
--boot = do
--  Options {..} <- asks options
--  liftDHT $ bootstrap (maybeToList optBootNode)

-- | Run bittorrent client with default options and log to @stderr@.
--
--   For testing purposes only.
--
simpleClient :: BitTorrent () -> IO ()
simpleClient m = do
  runStderrLoggingT $ LoggingT $ \ logger -> do
    withClient def logger (`runBitTorrent` m)

{-----------------------------------------------------------------------
--  Torrent identifiers
-----------------------------------------------------------------------}

class TorrentSource s where
  openHandle :: FilePath -> s -> BitTorrent Handle

instance TorrentSource InfoHash where
  openHandle path ih = openMagnet path (nullMagnet ih)
  {-# INLINE openHandle #-}

instance TorrentSource Magnet where
  openHandle = openMagnet
  {-# INLINE openHandle #-}

instance TorrentSource InfoDict where
  openHandle path dict = openTorrent path (nullTorrent dict)
  {-# INLINE openHandle #-}

instance TorrentSource Torrent where
  openHandle = openTorrent
  {-# INLINE openHandle #-}

instance TorrentSource FilePath where
  openHandle contentDir torrentPath = do
    t <- liftIO $ fromFile torrentPath
    openTorrent contentDir t
  {-# INLINE openHandle #-}

getIndex :: BitTorrent [Handle]
getIndex = do
  Client {..} <- getClient
  elems <$> liftIO (readMVar clientTorrents)