acme-not-a-joke-0.1.0.0: src/Acme/NotAJoke/Api/Order.hs
module Acme.NotAJoke.Api.Order where
import Control.Lens hiding ((.=))
import Data.Aeson (FromJSON (..), ToJSON (..), decode, encode, object, pairs, withObject, withText, (.:), (.:?), (.=))
import Data.ByteString.Lazy (ByteString)
import Data.Coerce (coerce)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Text.Encoding as Encoding
import Data.Time.Clock (UTCTime)
import qualified Network.Wreq as Wreq
import qualified Crypto.JOSE.JWS as JWS
import Acme.NotAJoke.Api.CSR
import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Field
import Acme.NotAJoke.Api.JWS
import Acme.NotAJoke.Api.Nonce
-- | RFC-defined order statuses.
data OrderStatus
= OrderPending
| OrderReady
| OrderProcessing
| OrderValid
| OrderInvalid
deriving (Show, Eq, Ord)
instance FromJSON OrderStatus where
parseJSON = withText "OrderStatus" $ \txt ->
case txt of
"pending" -> pure OrderPending
"ready" -> pure OrderReady
"processing" -> pure OrderProcessing
"valid" -> pure OrderValid
"invalid" -> pure OrderInvalid
_ -> fail $ "invalid order status:" <> show txt
-- | RFC-defined order types (for the subset supported in this library).
data OrderType
= -- | Order is about requesting DNS certificates.
DNSOrder
deriving (Show, Eq, Ord)
instance ToJSON OrderType where
toEncoding DNSOrder = toEncoding ("dns" :: Text)
toJSON DNSOrder = toJSON ("dns" :: Text)
instance FromJSON OrderType where
parseJSON = withText "Ordertype" $ \txt ->
case txt of
"dns" -> pure DNSOrder
_ -> fail $ "invalid ordertype:" <> show txt
-- | RFC-defined order identifier.
data OrderIdentifier
= OrderIdentifier
{ type_ :: OrderType
, value :: Text
}
deriving (Show, Eq, Ord)
instance ToJSON OrderIdentifier where
toEncoding o = pairs ("type" .= o.type_ <> "value" .= o.value)
toJSON o = object ["type" .= o.type_, "value" .= o.value]
instance FromJSON OrderIdentifier where
parseJSON = withObject "OrderIdentifier" $ \o ->
OrderIdentifier
<$> o .: "type"
<*> o .: "value"
-- | RFC-defined order structure.
data Order a
= Order
{ status :: Field a "status" OrderStatus
, expires :: Field a "expires" UTCTime
, identifiers :: Field a "identifiers" [OrderIdentifier]
, notBefore :: Field a "notBefore" (Maybe UTCTime)
, notAfter :: Field a "notAfter" (Maybe UTCTime)
, error :: Field a "error" (Maybe Problem)
, authorizations :: Field a "authorizations" [Url "authorization"]
, finalize :: Field a "finalize" (Url "finalize-order")
, certificate :: Field a "certificate" (Url "certificate")
}
type instance Field "order-create" "status" x = ()
type instance Field "order-create" "expires" x = ()
type instance Field "order-create" "identifiers" x = x
type instance Field "order-create" "notBefore" x = x
type instance Field "order-create" "notAfter" x = x
type instance Field "order-create" "error" x = ()
type instance Field "order-create" "authorizations" x = ()
type instance Field "order-create" "finalize" x = ()
type instance Field "order-create" "certificate" x = ()
newtype OrderCreated = OrderCreated (Wreq.Response ByteString)
deriving (Show)
type instance Field "order-created" "status" x = x
type instance Field "order-created" "expires" x = x
type instance Field "order-created" "identifiers" x = x
type instance Field "order-created" "notBefore" x = x
type instance Field "order-created" "notAfter" x = x
type instance Field "order-created" "error" x = x
type instance Field "order-created" "authorizations" x = x
type instance Field "order-created" "finalize" x = x
type instance Field "order-created" "certificate" x = ()
instance FromJSON (Order "order-created") where
parseJSON = withObject "Order(created)" $ \v ->
Order
<$> v .: "status"
<*> v .: "expires"
<*> v .: "identifiers"
<*> v .:? "notBefore"
<*> v .:? "notAfter"
<*> v .:? "error"
<*> v .: "authorizations"
<*> v .: "finalize"
<*> pure ()
-- | Prepare a new order.
createOrder :: (Maybe UTCTime, Maybe UTCTime) -> [OrderIdentifier] -> Order "order-create"
createOrder (nbefore, nafter) ois =
Order () () ois nbefore nafter () () () ()
readOrderUrl :: OrderCreated -> Maybe (Url "order")
readOrderUrl (OrderCreated rsp) = rsp ^? Wreq.responseHeader "location" . to (Url . Encoding.decodeUtf8)
readOrderCreated :: OrderCreated -> Maybe (Order "order-created")
readOrderCreated (OrderCreated rsp) = decode $ rsp ^. Wreq.responseBody
-- | Requests a new order to the server.
postNewOrder :: JWS.JWK -> Endpoint "newOrder" -> KID -> Nonce -> Order "order-create" -> IO (Maybe OrderCreated)
postNewOrder jwk ep kid nonce ord = do
let opts = Wreq.defaults & Wreq.header "Content-Type" .~ ["application/jose+json"]
ebody <- (kidSign jwk ep kid nonce $ encode $ serialized)
case ebody of
Right body -> do
e <- Wreq.postWith opts (wrequrl ep) $ encode body
pure $ Just $ OrderCreated e
Left err -> do
print err
pure Nothing
where
serialized =
object $
catMaybes
[ (\x -> "notBefore" .= x) <$> ord.notBefore
, (\x -> "notAfter" .= x) <$> ord.notAfter
, Just $ "identifiers" .= ord.identifiers
]
newtype OrderInspected = OrderInspected (Wreq.Response ByteString)
deriving (Show)
type instance Field "order-inspected" "status" x = x
type instance Field "order-inspected" "expires" x = x
type instance Field "order-inspected" "identifiers" x = x
type instance Field "order-inspected" "notBefore" x = ()
type instance Field "order-inspected" "notAfter" x = ()
type instance Field "order-inspected" "error" x = x
type instance Field "order-inspected" "authorizations" x = x
type instance Field "order-inspected" "finalize" x = x
type instance Field "order-inspected" "certificate" x = Maybe x
instance FromJSON (Order "order-inspected") where
parseJSON = withObject "Order(inspected)" $ \v ->
Order
<$> v .: "status"
<*> v .: "expires"
<*> v .: "identifiers"
<*> pure ()
<*> pure ()
<*> v .:? "error"
<*> v .: "authorizations"
<*> v .: "finalize"
<*> v .:? "certificate"
readOrderInspected :: OrderInspected -> Maybe (Order "order-inspected")
readOrderInspected (OrderInspected rsp) = decode $ rsp ^. Wreq.responseBody
readCertificateUrl :: OrderInspected -> Maybe (Url "certificate")
readCertificateUrl order =
certificate =<< readOrderInspected order
-- | Fetches a known order to inspect its status.
postGetOrder :: JWS.JWK -> Url "order" -> KID -> Nonce -> IO (Maybe OrderInspected)
postGetOrder jwk orderurl kid nonce = do
let opts = Wreq.defaults & Wreq.header "Content-Type" .~ ["application/jose+json"]
ebody <- (kidSign jwk ep kid nonce "")
case ebody of
Right body -> do
e <- Wreq.postWith opts (wrequrl ep) $ encode body
pure $ Just $ OrderInspected e
Left err -> do
print err
pure Nothing
where
ep :: Endpoint "order"
ep = coerce orderurl
{- | RFC-defined finalization request.
Consists of a CSR.
-}
data Finalize
= Finalize
{ csr :: CSR
}
newtype OrderFinalized = OrderFinalized (Wreq.Response ByteString)
deriving (Show)
-- todo: specialize status of a finalized order
readOrderFinalized :: OrderFinalized -> Maybe (Order "order-created")
readOrderFinalized (OrderFinalized rsp) = decode $ rsp ^. Wreq.responseBody
-- | Finalize an order after completing a challenge.
postFinalizeOrder :: JWS.JWK -> KID -> Url "finalize-order" -> Finalize -> Nonce -> IO (Maybe OrderFinalized)
postFinalizeOrder jwk kid finalizeurl finalizeobj nonce = do
let opts = Wreq.defaults & Wreq.header "Content-Type" .~ ["application/jose+json"]
ebody <- (kidSign jwk ep kid nonce $ encode $ serialized)
case ebody of
Right body -> do
e <- Wreq.postWith opts (wrequrl ep) $ encode body
pure $ Just $ OrderFinalized e
Left err -> do
print err
pure Nothing
where
ep :: Endpoint "finalize-order"
ep = coerce finalizeurl
serialized =
object $
[ "csr" .= finalizeobj.csr
]