From 575fe571aa33bdaa1cc4e96b19482eb10d00f32a Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Tue, 30 May 2023 13:05:26 -0400
Subject: [PATCH] wip some steps towards StorageServer/StorageClient
 refactoring

---
 gbs-downloader.cabal  |   1 +
 src/Tahoe/Client.hs   | 138 ++++++++++++++++++++++++++++++++++++++++
 src/Tahoe/Download.hs | 143 +++---------------------------------------
 3 files changed, 147 insertions(+), 135 deletions(-)
 create mode 100644 src/Tahoe/Client.hs

diff --git a/gbs-downloader.cabal b/gbs-downloader.cabal
index c6a8896..ef28645 100644
--- a/gbs-downloader.cabal
+++ b/gbs-downloader.cabal
@@ -80,6 +80,7 @@ library
   -- Modules exported by the library.
   exposed-modules:
     Tahoe.Announcement
+    Tahoe.Client
     Tahoe.Download
 
   -- Modules included in this library but not exported.
diff --git a/src/Tahoe/Client.hs b/src/Tahoe/Client.hs
new file mode 100644
index 0000000..e23de90
--- /dev/null
+++ b/src/Tahoe/Client.hs
@@ -0,0 +1,138 @@
+module Tahoe.Client where
+
+import Control.Exception (SomeException (SomeException))
+import Control.Monad.IO.Class (MonadIO)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Base64 as Base64
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import Network.HTTP.Client (Manager, ManagerSettings (managerModifyRequest), Request (requestHeaders))
+import Network.URI (URI (URI, uriAuthority, uriFragment, uriPath, uriScheme), URIAuth (URIAuth, uriPort, uriRegName, uriUserInfo))
+import Servant.Client (BaseUrl (BaseUrl, baseUrlHost, baseUrlPath, baseUrlPort, baseUrlScheme))
+import Tahoe.Announcement (StorageServerAnnouncement, greatBlackSwampURIs)
+import TahoeLAFS.Storage.API (CBORSet (CBORSet), ShareNumber (ShareNumber))
+import Text.Read (readMaybe)
+
+-- | There was a problem while trying to look up a server from its announcement.
+data LookupError
+    = -- | The server's announced URI was unparseable.
+      URIParseError StorageServerAnnouncement
+    | -- | The port integer in the server's URI was unparseable.
+      PortParseError String
+    | -- | The structure of the server's URI was unparseable.
+      AnnouncementStructureUnmatched
+    deriving (Eq, Ord, Show)
+
+data StorageClient = StorageClient
+    { storageClientManager :: Manager
+    , storageClientBaseUrl :: BaseUrl
+    }
+
+makeGBSManager :: MonadIO m => URI -> m (Either LookupError Manager)
+makeGBSManager
+    URI
+        { uriScheme = "pb:"
+        , uriAuthority = Just URIAuth{uriUserInfo = tubid, uriRegName = host, uriPort = (':' : port)}
+        , uriPath = ('/' : swissnum)
+        , uriFragment = "" -- It's a fURL, not a NURL, so there's no fragment.
+        } =
+        case readMaybe port of
+            Nothing -> pure . Left . PortParseError $ port
+            Just realPort -> do
+                manager <- newGBSManager tubid swissnum
+                wrapGreatBlackSwamp
+
+-- | Make a manager suitable for use with a Great Black Swamp server.
+newGBSManager ::
+    MonadIO m =>
+    [Char] ->
+    String ->
+    m Manager
+newGBSManager tubid swissnum =
+    newTlsManagerWith $
+        managerSettingsForService
+            (T.pack . init $ tubid)
+            (T.pack swissnum)
+
+--                 pure . Right $ wrapGreatBlackSwamp manager host realPort
+-- makeServer _ = pure . Left $ AnnouncementStructureUnmatched
+
+-- Parameterize readImmutableShare and getImmutableShareNumbers to
+-- wrapGreatBlackSwamp, then use it for both mutables and immutables.  Then
+-- use the same download function for both!
+
+{- | Create a StorageClient that will speak Great Black Swamp using the given
+ manager to the server at the given host/port.
+-}
+wrapGreatBlackSwamp :: [Char] -> Int -> Manager -> StorageServer
+wrapGreatBlackSwamp host realPort manager =
+    StorageClient{..}
+  where
+    baseUrl = https host realPort
+    env = mkClientEnv manager baseUrl
+    toBase32 = T.unpack . T.toLower . encodeBase32Unpadded
+
+    storageServerID = undefined
+
+    storageServerWrite = undefined
+
+    storageServerRead storageIndex shareNum = do
+        let clientm = readImmutableShare (toBase32 storageIndex) (ShareNumber $ fromIntegral shareNum) Nothing
+        res <- runClientM clientm env
+        case res of
+            Left err -> throwIO err
+            Right bs -> pure bs
+
+    storageServerGetBuckets storageIndex = do
+        let clientm = getImmutableShareNumbers (toBase32 storageIndex)
+        print' "Going to get share numbers"
+        r <- try $ runClientM clientm env
+        case r of
+            Left (err :: SomeException) -> do
+                pure mempty
+            Right res -> do
+                case res of
+                    Left err -> throwIO err
+                    Right (CBORSet s) -> pure $ Set.map (\(ShareNumber i) -> fromIntegral i) s -- XXX fromIntegral aaaaaaaa!!
+
+-- | Make an HTTPS URL.
+https :: String -> Int -> BaseUrl
+https host port =
+    BaseUrl
+        { baseUrlScheme = Https
+        , baseUrlHost = host
+        , baseUrlPort = port
+        , baseUrlPath = ""
+        }
+
+{- | Make an HTTPS manager for the given SPKI hash and swissnum.
+
+ The SPKI hash is _not_ used to authenticate the server!  See
+ https://whetstone.private.storage/privatestorage/tahoe-great-black-swamp/-/issues/27
+-}
+managerSettingsForService :: T.Text -> T.Text -> ManagerSettings
+managerSettingsForService _ swissnum =
+    (mkManagerSettings tlsSettings sockSettings){managerModifyRequest = pure . authorize}
+  where
+    tlsSettings = TLSSettingsSimple True True True
+    sockSettings = Nothing
+    swissnumBytes = encodeUtf8 swissnum
+    swissnumBase64 = Base64.encode swissnumBytes
+    headerCompleteBytes = B.concat ["Tahoe-LAFS ", swissnumBase64]
+    authorize req =
+        req
+            { requestHeaders =
+                ( "Authorization"
+                , headerCompleteBytes
+                ) :
+                requestHeaders req
+            }
+
+{- | Interpret the location in an announcement as a Tahoe-LAFS fURL pointed at
+ a Great Black Swamp server.
+-}
+announcementToStorageServer :: MonadIO m => StorageServerAnnouncement -> m (Either LookupError StorageServer)
+announcementToStorageServer ann =
+    case greatBlackSwampURIs ann of
+        Nothing -> pure . Left . URIParseError $ ann
+        Just uri -> makeServer uri
diff --git a/src/Tahoe/Download.hs b/src/Tahoe/Download.hs
index 5b25fd3..629e977 100644
--- a/src/Tahoe/Download.hs
+++ b/src/Tahoe/Download.hs
@@ -36,8 +36,8 @@ import Tahoe.Announcement (StorageServerAnnouncement, greatBlackSwampURIs)
 import qualified Tahoe.CHK
 import Tahoe.CHK.Capability (Reader (..), Verifier (..))
 import qualified Tahoe.CHK.Encrypt
-import Tahoe.CHK.Server (StorageServer (..), StorageServerID)
 import Tahoe.CHK.Types (ShareNum, StorageIndex)
+import Tahoe.Client (StorageClient (..), StorageServerID)
 import qualified Tahoe.SDMF as SDMF
 import TahoeLAFS.Storage.API (CBORSet (..), ShareNumber (ShareNumber))
 import TahoeLAFS.Storage.Client (getImmutableShareNumbers, readImmutableShare)
@@ -48,7 +48,7 @@ print' :: MonadIO m => String -> m ()
 print' = const $ pure ()
 
 -- | Partially describe one share download.
-type DownloadTask = (ShareNum, StorageServer)
+type DownloadTask = (ShareNum, StorageClient)
 
 -- | A downloaded share
 type Share = (ShareNum, LB.ByteString)
@@ -102,25 +102,22 @@ data DiscoverError
 -}
 type LookupServer m = StorageServerAnnouncement -> m (Either LookupError StorageServer)
 
-downloadMutable :: MonadIO m => Map.Map StorageServerID StorageServerAnnouncement -> SDMF.Reader -> LookupServer m -> m (Either DownloadError LB.ByteString)
-downloadMutable = undefined
-
 {- | Recover the application data associated with a given capability from the
  given servers, if possible.
 -}
 download ::
-    MonadIO m =>
+    (ReadCapability r, MonadIO m) =>
     -- | Information about the servers from which to consider downloading shares
     -- representing the application data.
     Map.Map StorageServerID StorageServerAnnouncement ->
     -- | The read capability for the application data.
-    Reader ->
+    r ->
     -- | Get functions for interacting with a server given its URL.
     LookupServer m ->
     -- | Either a description of how the recovery failed or the recovered
     -- application data.
     m (Either DownloadError LB.ByteString)
-download servers cap@Reader{verifier = Verifier{..}} lookupServer = do
+download servers cap lookupServer = do
     print' ("Going to download: " <> show storageIndex)
     locationE <- locateShares servers lookupServer storageIndex required
     print' "Finished locating shares"
@@ -263,130 +260,6 @@ downloadShare storageIndex (shareNum, s) = do
     print' "Downloaded it"
     pure (shareNum, LB.fromStrict <$> massaged)
 
--- | There was a problem while trying to look up a server from its announcement.
-data LookupError
-    = -- | The server's announced URI was unparseable.
-      URIParseError StorageServerAnnouncement
-    | -- | The port integer in the server's URI was unparseable.
-      PortParseError String
-    | -- | The structure of the server's URI was unparseable.
-      AnnouncementStructureUnmatched
-    deriving (Eq, Ord, Show)
-
-{- | Interpret the location in an announcement as a Tahoe-LAFS fURL pointed at
- a Great Black Swamp server.
--}
-announcementToStorageServer :: MonadIO m => StorageServerAnnouncement -> m (Either LookupError StorageServer)
-announcementToStorageServer ann =
-    case greatBlackSwampURIs ann of
-        Nothing -> pure . Left . URIParseError $ ann
-        Just uri -> makeServer uri
-
-makeServer :: MonadIO m => URI -> m (Either LookupError StorageServer)
-makeServer
-    URI
-        { uriScheme = "pb:"
-        , uriAuthority = Just URIAuth{uriUserInfo = tubid, uriRegName = host, uriPort = (':' : port)}
-        , uriPath = ('/' : swissnum)
-        , uriFragment = "" -- It's a fURL, not a NURL, so there's no fragment.
-        } =
-        case readMaybe port of
-            Nothing -> pure . Left . PortParseError $ port
-            Just realPort -> do
-                print' "Going to make a GBS manager"
-                manager <- liftIO $ newGBSManager tubid swissnum
-                print' "Made it"
-
-                pure . Right $ wrapGreatBlackSwamp manager host realPort
-makeServer _ = pure . Left $ AnnouncementStructureUnmatched
-
--- Parameterize readImmutableShare and getImmutableShareNumbers to
--- wrapGreatBlackSwamp, then use it for both mutables and immutables.  Then
--- use the same download function for both!
-
-{- | Create a StorageServer that will speak Great Black Swamp using the given
- manager to the server at the given host/port.
--}
-wrapGreatBlackSwamp :: Manager -> [Char] -> Int -> StorageServer
-wrapGreatBlackSwamp manager host realPort =
-    StorageServer{..}
-  where
-    baseUrl = https host realPort
-    env = mkClientEnv manager baseUrl
-    toBase32 = T.unpack . T.toLower . encodeBase32Unpadded
-
-    storageServerID = undefined
-
-    storageServerWrite = undefined
-
-    storageServerRead storageIndex shareNum = do
-        let clientm = readImmutableShare (toBase32 storageIndex) (ShareNumber $ fromIntegral shareNum) Nothing
-        print' "Going to read from a server"
-        res <- runClientM clientm env
-        print' "Did it"
-        case res of
-            Left err -> do
-                print' "Going to throw a damn IO error"
-                throwIO err
-            Right bs -> pure bs
-
-    storageServerGetBuckets storageIndex = do
-        let clientm = getImmutableShareNumbers (toBase32 storageIndex)
-        print' "Going to get share numbers"
-        r <- try $ runClientM clientm env
-        case r of
-            Left (err :: SomeException) -> do
-                print' $ "A PROBLEM ARISES " <> show err
-                pure mempty
-            Right res -> do
-                print' "Got the share numbers"
-                case res of
-                    Left err -> do
-                        print' "Going to throw another IO error!!"
-                        throwIO err
-                    Right (CBORSet s) -> pure $ Set.map (\(ShareNumber i) -> fromIntegral i) s -- XXX fromIntegral aaaaaaaa!!
-
--- | Make an HTTPS URL.
-https :: String -> Int -> BaseUrl
-https host port =
-    BaseUrl
-        { baseUrlScheme = Https
-        , baseUrlHost = host
-        , baseUrlPort = port
-        , baseUrlPath = ""
-        }
-
-{- | Make an HTTPS manager for the given SPKI hash and swissnum.
-
- The SPKI hash is _not_ used to authenticate the server!  See
- https://whetstone.private.storage/privatestorage/tahoe-great-black-swamp/-/issues/27
--}
-managerSettingsForService :: T.Text -> T.Text -> ManagerSettings
-managerSettingsForService _ swissnum =
-    (mkManagerSettings tlsSettings sockSettings){managerModifyRequest = pure . authorize}
-  where
-    tlsSettings = TLSSettingsSimple True True True
-    sockSettings = Nothing
-    swissnumBytes = encodeUtf8 swissnum
-    swissnumBase64 = Base64.encode swissnumBytes
-    headerCompleteBytes = B.concat ["Tahoe-LAFS ", swissnumBase64]
-    authorize req =
-        req
-            { requestHeaders =
-                ( "Authorization"
-                , headerCompleteBytes
-                ) :
-                requestHeaders req
-            }
-
--- | Make a manager suitable for use with a Great Black Swamp server.
-newGBSManager ::
-    MonadIO m =>
-    [Char] ->
-    String ->
-    m Manager
-newGBSManager tubid swissnum =
-    newTlsManagerWith $
-        managerSettingsForService
-            (T.pack . init $ tubid)
-            (T.pack swissnum)
+class ReadCapability r s | r -> s where
+    storageIndex :: r -> StorageIndex
+    decode :: LB.ByteString -> Maybe s
-- 
GitLab