diff --git a/src/Tahoe/Download.hs b/src/Tahoe/Download.hs index b4fb3c92fd1b7e081252d189c4a377285b66c7f1..08f305d9ddf81ef2f36cfb20044af86fe156bb64 100644 --- a/src/Tahoe/Download.hs +++ b/src/Tahoe/Download.hs @@ -16,7 +16,8 @@ module Tahoe.Download ( import Control.Exception (throwIO) import Control.Monad.IO.Class (MonadIO (liftIO)) -import Data.Binary (decodeOrFail) +import Data.Binary (Word16, decodeOrFail) +import qualified Data.ByteString as B import Data.ByteString.Base32 (encodeBase32Unpadded) import Data.ByteString.Base64 (encodeBase64) import qualified Data.ByteString.Lazy as LB @@ -83,10 +84,9 @@ data DiscoverError | StorageServerUnreachable LookupError deriving (Eq, Ord, Show) --- TODO The result might need to be in IO in case the URL indicates a --- Tor-based route to the server. In this case we might need to launch a Tor --- daemon or connect to a running Tor daemon or at least set up a new Tor --- circuit. All of which require I/O. But we can always refactor later! +{- | The type of a function that can produce a concrete StorageServer from + that server's announcement. +-} type LookupServer m = StorageServerAnnouncement -> m (Either LookupError StorageServer) {- | Recover the application data associated with a given capability from the @@ -104,7 +104,31 @@ download :: -- | Either a description of how the recovery failed or the recovered -- application data. m (Either DownloadError LB.ByteString) -download servers cap@Reader{readKey, verifier = Verifier{..}} lookupServer = +download servers cap@Reader{verifier = Verifier{..}} lookupServer = do + locationE <- locateShares servers lookupServer storageIndex required + case locationE of + Left err -> pure $ Left err + Right discovered -> do + -- XXX note shares can contain failures + shares <- fetchShares storageIndex discovered + decodeShares cap shares + +-- | Find out which servers claim to have shares related to a given storage index. +locateShares :: + MonadIO m => + -- | Information about the servers from which to consider downloading shares + -- representing the application data. + Map.Map StorageServerID StorageServerAnnouncement -> + -- | Get functions for interacting with a server given its URL. + LookupServer m -> + -- | The storage index about which to retrieve information. + B.ByteString -> + -- | The number of shares we need to locate. If we cannot find at least + -- this many shares the result will be an error. + Word16 -> + -- | Either an error or a guide to where shares are placed. + m (Either DownloadError [(StorageServer, Set.Set ShareNum)]) +locateShares servers lookupServer storageIndex required = case Map.toList servers of [] -> pure . Left $ NoConfiguredServers serverList -> do @@ -118,24 +142,46 @@ download servers cap@Reader{readKey, verifier = Verifier{..}} lookupServer = else if (fromIntegral required >) . countDistinctShares $ discovered then pure $ Left NotEnoughShares{notEnoughSharesNeeded = fromIntegral required, notEnoughSharesFound = countDistinctShares discovered} - else do - -- XXX up to here is probably "locateShares". now we move in to "fetchShares". - let sharemap = makeShareMap discovered - -- XXX note shares can contain failures - shares <- mapM (uncurry $ downloadShare storageIndex) (Map.toList sharemap) - -- XXX That was brief. Probably missing some complexity. now we move on to "decodeShares". - -- Filter down to shares we actually got. - let someShares = filter (isRight . snd) shares - fewerShares = filter (isRight . snd) $ (\(sharenum, Right bs) -> (sharenum, decodeOrFail bs)) <$> someShares - onlyDecoded = (\(sharenum, Right (_, _, share)) -> (fromIntegral sharenum, share)) <$> fewerShares - if length onlyDecoded < fromIntegral required - then pure $ Left NotEnoughDecodedShares{notEnoughDecodedSharesNeeded = fromIntegral required, notEnoughDecodedSharesFound = length onlyDecoded} - else do - ciphertext <- liftIO $ Tahoe.CHK.decode cap onlyDecoded - case ciphertext of - Nothing -> pure $ Left ShareDecodingFailed - Just ct -> - pure . Right $ Tahoe.CHK.Encrypt.decrypt readKey ct + else pure $ Right discovered + +{- | Given a guide to where shares for a given storage index are placed, + download them. +-} +fetchShares :: + MonadIO m => + -- | The storage index of the shares to download. + B.ByteString -> + -- | The guide to where shares are placed. + [(StorageServer, Set.Set ShareNum)] -> + m [(ShareNum, Either DownloadError LB.ByteString)] +fetchShares storageIndex discovered = + mapM (uncurry $ downloadShare storageIndex) (Map.toList sharemap) + where + sharemap = makeShareMap discovered + +{- | Given the results of downloading shares related to a given capability, + decode them and decrypt the contents of possible. +-} +decodeShares :: + MonadIO m => + -- | The read capability which allows the contents to be decrypted. + Reader -> + -- | The results of downloading the shares. + [(ShareNum, Either DownloadError LB.ByteString)] -> + m (Either DownloadError LB.ByteString) +decodeShares cap@Reader{readKey, verifier = Verifier{..}} shares = + -- Filter down to shares we actually got. + let someShares = filter (isRight . snd) shares + fewerShares = filter (isRight . snd) $ (\(sharenum, Right bs) -> (sharenum, decodeOrFail bs)) <$> someShares + onlyDecoded = (\(sharenum, Right (_, _, share)) -> (fromIntegral sharenum, share)) <$> fewerShares + in if length onlyDecoded < fromIntegral required + then pure $ Left NotEnoughDecodedShares{notEnoughDecodedSharesNeeded = fromIntegral required, notEnoughDecodedSharesFound = length onlyDecoded} + else do + ciphertext <- liftIO $ Tahoe.CHK.decode cap onlyDecoded + case ciphertext of + Nothing -> pure $ Left ShareDecodingFailed + Just ct -> + pure . Right $ Tahoe.CHK.Encrypt.decrypt readKey ct {- | Figure the total number of distinct shares reported by all of the servers we asked.