packages feed

acme-not-a-joke-0.1.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 Control.Lens hiding ((.=))
import Data.Aeson (FromJSON (..), ToJSON (..))
import Data.ByteString.Lazy (ByteString)
import Data.Coerce (Coercible, coerce)
import Data.IORef (atomicModifyIORef, newIORef, writeIORef)
import Data.Text (Text)
import qualified Data.Text.Encoding as Encoding
import qualified Network.Wreq as Wreq

import Acme.NotAJoke.Api.Endpoint

newtype Nonce = Nonce Text
    deriving (Show, FromJSON, ToJSON)

getNonce :: Endpoint "newNonce" -> IO (Maybe Nonce)
getNonce ep = do
    r <- Wreq.head_ (wrequrl ep)
    pure $ responseNonceWreq r
  where

responseNonceWreq :: forall a. Wreq.Response a -> Maybe Nonce
responseNonceWreq r =
    r ^? Wreq.responseHeader "replay-nonce" . to Encoding.decodeUtf8 . to Nonce

responseNonceWreqBS :: Wreq.Response ByteString -> Maybe Nonce
responseNonceWreqBS = responseNonceWreq

responseNonce :: forall a. (Coercible a (Wreq.Response ByteString)) => a -> Maybe Nonce
responseNonce = responseNonceWreqBS . coerce

data Fetcher = Fetcher
    { produce :: IO (Maybe Nonce)
    , set :: Nonce -> IO ()
    , fetchNewNonce :: IO (Maybe Nonce)
    }

fetcher :: IO (Maybe 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 (Just x)

saveResponseNonce :: forall a. (Coercible a (Wreq.Response ByteString)) => Fetcher -> a -> IO ()
saveResponseNonce nonceFetcher rsp =
    maybe (pure ()) (nonceFetcher.set) (responseNonce rsp)

saveNonce :: forall a. (Coercible a (Wreq.Response ByteString)) => Fetcher -> IO (Maybe a) -> IO (Maybe a)
saveNonce nonceFetcher apiCall = do
    obj <- apiCall
    maybe (pure ()) (saveResponseNonce nonceFetcher) obj
    pure obj