diff --git a/gbs-downloader.cabal b/gbs-downloader.cabal index 3f4350fa8bfb7decda8d7c5139168c7aaa0f1fbf..6bb1f36ef7a0183555d418421585f1eb1a618e09 100644 --- a/gbs-downloader.cabal +++ b/gbs-downloader.cabal @@ -66,6 +66,7 @@ common language NamedFieldPuns OverloadedStrings RecordWildCards + TupleSections library -- Import common warning flags. @@ -85,6 +86,7 @@ library -- Other library packages from which modules are imported. build-depends: , base ^>=4.14.3.0 + , binary , bytestring , containers , tahoe-chk diff --git a/src/Tahoe/Download.hs b/src/Tahoe/Download.hs index f5a05d6a3f33cf0aa1af124657913dccb1d3a678..24e6715a99debf1947aac0de7278edc70a39c787 100644 --- a/src/Tahoe/Download.hs +++ b/src/Tahoe/Download.hs @@ -1,13 +1,14 @@ module Tahoe.Download (DownloadError (..), download) where +import Data.Binary (decodeOrFail) import qualified Data.ByteString.Lazy as LB -import Data.Either (rights) +import Data.Either (isRight, rights) import Data.List (foldl') import qualified Data.Map.Strict as Map import qualified Data.Set as Set -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) +import qualified Tahoe.CHK import Tahoe.CHK.Capability (Reader (..), Verifier (..)) +import qualified Tahoe.CHK.Encrypt import Tahoe.CHK.Server (StorageServer (..), StorageServerAnnouncement (..), StorageServerID, URL) import Tahoe.CHK.Types (ShareNum, StorageIndex) @@ -20,11 +21,23 @@ data DownloadError | -- | Across all of the configured servers, none were actually connectable. NoReachableServers | -- | Across all of the configured servers, fewer than the required - -- number of shares were found. + -- number of shares were found. XXX Could split this into the different + -- cases - did not locate enough shares, did not download enough shares, + -- did not decode enough shares, did not verify enough shares NotEnoughShares { notEnoughSharesNeeded :: Int , notEnoughSharesFound :: Int } + | ShareDecodingFailed + deriving (Eq, Ord, Show) + +{- | An problem arose while attempting to discover the shares held on a + particular server. +-} +data DiscoverError + = -- | An announcement did not include a location for a connection attempt. + StorageServerLocationUnknown + | StorageServerUnreachable deriving (Eq, Ord, Show) {- | Recover the application data associated with a given capability from the @@ -41,18 +54,35 @@ download :: -- | Either a description of how the recovery failed or the recovered -- application data. IO (Either DownloadError LB.ByteString) -download servers Reader{verifier = Verifier{..}} openServer = +download servers cap@Reader{readKey, verifier = Verifier{..}} openServer = case Map.toList servers of [] -> pure . Left $ NoConfiguredServers serverList -> do -- Ask each server for all shares it has. - discovered <- rights <$> mapM discoverOnce serverList :: IO [(StorageServerID, Set.Set ShareNum)] + discovered <- rights <$> mapM discoverOnce serverList :: IO [(StorageServer, Set.Set ShareNum)] if null discovered then pure $ Left NoReachableServers else if (fromIntegral required >=) . countDistinctShares $ discovered then pure $ Left NotEnoughShares{notEnoughSharesNeeded = fromIntegral required, notEnoughSharesFound = countDistinctShares discovered} - else pure . Right . LB.fromStrict . encodeUtf8 . T.pack . show $ 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 NotEnoughShares{notEnoughSharesNeeded = fromIntegral required, notEnoughSharesFound = length onlyDecoded} + else do + ciphertext <- Tahoe.CHK.decode cap onlyDecoded + case ciphertext of + Nothing -> pure $ Left ShareDecodingFailed + Just ct -> + pure . Right $ Tahoe.CHK.Encrypt.decrypt readKey ct where -- Figure the total number of distinct shares reported by all of the servers we -- asked. @@ -60,39 +90,35 @@ download servers Reader{verifier = Verifier{..}} openServer = -- Ask one server which shares it has related to the storage index in -- question. - discoverOnce (sid, sann) = do - sharenums <- discoverShares storageIndex sann openServer - pure $ case sharenums of - Left e -> Left e - Right shnums -> Right (sid, shnums) + discoverOnce (_sid, sann) = do + case storageServerAnnouncementFURL sann of + Nothing -> pure $ Left StorageServerLocationUnknown + Just url -> do + server <- openServer url + case server of + Nothing -> pure $ Left StorageServerUnreachable + Just ss@StorageServer{storageServerGetBuckets} -> + Right . (ss,) <$> storageServerGetBuckets storageIndex -{- | An problem arose while attempting to discover the shares held on a - particular server. +{- | Invert the mapping implied by the list of two tuples so that the servers + that claim to have a certain share can easily be retrieved. -} -data DiscoverError - = -- | An announcement did not include a location for a connection attempt. - StorageServerLocationUnknown - | StorageServerUnreachable - deriving (Eq, Ord, Show) +makeShareMap :: Ord k => [(v, Set.Set k)] -> Map.Map k [v] +makeShareMap locations = + foldl' (Map.unionWith (<>)) mempty ((\(k, v) -> Map.fromSet (\_ -> [k]) v) <$> locations) --- | Identify which servers claim to have some data at some index. -discoverShares :: - -- | The storage index at which to look for data. +-- | Download the bytes of a share from one (or more!) of the given servers. +downloadShare :: + -- | The storage index of the share to download. StorageIndex -> - -- | A server which could possibly have the data. It "could possibly" - -- have the data because local configuration suggests the data might have - -- been uploaded to them in the past. - StorageServerAnnouncement -> - -- | A function to connect to a server. - (URL -> IO (Maybe StorageServer)) -> - -- | The share numbers the server claims to have. - IO (Either DiscoverError (Set.Set ShareNum)) -discoverShares storageIndex ann openServer = - case storageServerAnnouncementFURL ann of - Nothing -> pure $ Left StorageServerLocationUnknown - Just url -> do - server <- openServer url - case server of - Nothing -> pure $ Left StorageServerUnreachable - Just StorageServer{storageServerGetBuckets} -> - Right <$> storageServerGetBuckets storageIndex + -- | The number of the share to download. + ShareNum -> + -- | The servers which we have a reasonable belief could hold a copy of the + -- share. It is common for there to be only one server holding each share + -- but nothing *prevents* multiple servers from having one. In this case we + -- could download the share from both of them, perhaps. + [StorageServer] -> + -- | The bytes of the share or some error that was encountered during + -- download. + IO (ShareNum, Either DownloadError LB.ByteString) +downloadShare = undefined diff --git a/test/Spec.hs b/test/Spec.hs index 74566683bf79262165d72f1c34f878985009d099..4251dece5429a402609148124e1adfd5402daef4 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -128,6 +128,16 @@ tests = "download should fail with not enough shares" (Left NotEnoughShares{notEnoughSharesNeeded = 3, notEnoughSharesFound = 2}) result + , testCase "success" $ do + -- If we can recover enough distinct, decodeable shares from the + -- configured servers then we can recover the application data. + -- + -- XXX Start writing here. Probably make this a property test + -- that generates lots of configurations where it should be + -- possible to recover the data (have all the shares, have enough + -- of the shares, spread them across many servers, concentrate + -- them on one or a few, etc) + pure () ] where noServers _ = pure Nothing