packages feed

acme-not-a-joke-0.2.0.0: src/Acme/NotAJoke/Api/Nonce.hs

{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

{- | Almost all ACME API Calls require a Nonce to prevent replayability of
requests.
Most API Calls return a Nonce for the next request.
Client should re-use these Nonce to avoid overloading the server.
This module provide helpers to deal with this requirement.
-}
module Acme.NotAJoke.Api.Nonce where

import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.Coerce (Coercible, coerce)
import Data.IORef (atomicModifyIORef, newIORef, writeIORef)
import Data.Text (Text)
import qualified Data.Text.Encoding as Encoding

import Acme.NotAJoke.Api.Endpoint

-- | An anti-replay nonce, valid for one request.
newtype Nonce = Nonce Text
    deriving (Show, FromJSON, ToJSON)

-- | Asks the server for a fresh nonce (at the @newNonce@ endpoint of the directory).
getNonce :: Endpoint "newNonce" -> IO (Either AcmeError Nonce)
getNonce ep = do
    r <- head_ ep
    pure $ readNonce =<< r
  where
    readNonce rsp =
        maybe (Left $ UnexpectedResponse "no replay-nonce header" rsp) Right $
            rawResponseNonce rsp

-- | Lookup the nonce of a response, in its @Replay-Nonce@ header.
rawResponseNonce :: Response -> Maybe Nonce
rawResponseNonce rsp =
    Nonce . Encoding.decodeUtf8 <$> responseHeader "replay-nonce" rsp

-- | Same as 'rawResponseNonce' for the newtypes wrapping a 'Response'.
responseNonce :: forall a. (Coercible a Response) => a -> Maybe Nonce
responseNonce = rawResponseNonce . coerce

{- | A source of nonces which keeps the last nonce returned by the server and
fetches a fresh one only when it has none.
-}
data Fetcher = Fetcher
    { produce :: IO (Either AcmeError Nonce)
    -- ^ returns the saved nonce (forgetting it), or fetches a fresh one
    , set :: Nonce -> IO ()
    -- ^ saves a nonce for the next call to 'produce'
    , fetchNewNonce :: IO (Either AcmeError Nonce)
    -- ^ fetches a fresh nonce, ignoring the saved one
    }

-- | Builds a 'Fetcher' from a way to fetch fresh nonces (e.g., 'getNonce').
fetcher :: IO (Either AcmeError Nonce) -> IO Fetcher
fetcher fetch = do
    ref <- newIORef Nothing
    pure $ Fetcher (go ref) (writeIORef ref . Just) fetch
  where
    go ref = do
        val <- atomicModifyIORef ref (\x -> (Nothing, x))
        case val of
            Nothing -> fetch
            (Just x) -> pure (Right x)

-- | Saves the nonce of a response (if any) in a 'Fetcher'.
saveResponseNonce :: forall a. (Coercible a Response) => Fetcher -> a -> IO ()
saveResponseNonce nonceFetcher rsp =
    maybe (pure ()) (nonceFetcher.set) (responseNonce rsp)

{- | Saves the nonce found in the response of an API call.
Error responses from the server carry a fresh nonce as well, which we save too.
-}
saveNonce :: forall a. (Coercible a Response) => Fetcher -> IO (Either AcmeError a) -> IO (Either AcmeError a)
saveNonce nonceFetcher apiCall = do
    obj <- apiCall
    case obj of
        Right rsp -> saveResponseNonce nonceFetcher rsp
        Left err -> maybe (pure ()) (saveResponseNonce nonceFetcher) (errorResponse err)
    pure obj