summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-25 17:31:37 -0500
committerAndrew Cady <d@jerkface.net>2016-01-25 17:31:37 -0500
commitcf440860e186e7fd775ae27da08220d9fe5e233e (patch)
tree655fcfc417730de6616621928a817f361b40fa0d
parentbe7a90d29c8a6da269d54f355cee11ce6e5eabc2 (diff)
Change API of "certify"
It now expects a callback to provision the challenge responses. This needs to be improved so that it will also do cleanup.
-rw-r--r--acme-certify.hs15
-rw-r--r--src/Network/ACME.hs35
2 files changed, 34 insertions, 16 deletions
diff --git a/acme-certify.hs b/acme-certify.hs
index d20c3e4..cda3d09 100644
--- a/acme-certify.hs
+++ b/acme-certify.hs
@@ -13,8 +13,7 @@ module Main where
13 13
14import BasePrelude 14import BasePrelude
15import qualified Data.ByteString.Lazy.Char8 as LC 15import qualified Data.ByteString.Lazy.Char8 as LC
16import Network.ACME (CSR (..), canProvision, certify, 16import Network.ACME (CSR (..), canProvision, certify, fileProvisioner, ensureWritableDir, (</>), domainToString)
17 ensureWritableDir, (</>), domainToString)
18import Network.ACME.Encoding (Keys (..), readKeys, toStrict) 17import Network.ACME.Encoding (Keys (..), readKeys, toStrict)
19import Network.URI 18import Network.URI
20import OpenSSL 19import OpenSSL
@@ -106,7 +105,7 @@ genReq (Keys priv pub) domains@(domain:_) = withOpenSSL $ do
106 setPublicKey req pub 105 setPublicKey req pub
107 void $ addExtensions req [(nidSubjectAltName, intercalate ", " (map (("DNS:" ++) . domainToString) domains))] 106 void $ addExtensions req [(nidSubjectAltName, intercalate ", " (map (("DNS:" ++) . domainToString) domains))]
108 signX509Req req priv (Just dig) 107 signX509Req req priv (Just dig)
109 CSR . toStrict <$> writeX509ReqDER req 108 CSR domains . toStrict <$> writeX509ReqDER req
110 where 109 where
111 nidSubjectAltName = 85 110 nidSubjectAltName = 85
112 111
@@ -133,14 +132,16 @@ go CmdOpts { .. } = do
133 challengeDir <- ensureWritableDir optChallengeDir "challenge directory" 132 challengeDir <- ensureWritableDir optChallengeDir "challenge directory"
134 void $ ensureWritableDir domainDir "domain directory" 133 void $ ensureWritableDir domainDir "domain directory"
135 134
136 forM_ requestDomains $ canProvision challengeDir >=> 135 let skipProvisionCheck = True
137 (`unless` error "Error: cannot provision files to web server via challenge directory") 136 unless skipProvisionCheck $
137 forM_ requestDomains $ canProvision challengeDir >=>
138 (`unless` error "Error: cannot provision files to web server via challenge directory")
138 139
139 csrData <- genReq domainKeys requestDomains 140 certReq <- genReq domainKeys requestDomains
140 141
141 let email = either (error . ("Error: invalid email address: " ++)) id . validate . fromString <$> optEmail 142 let email = either (error . ("Error: invalid email address: " ++)) id . validate . fromString <$> optEmail
142 143
143 certificate <- certify directoryUrl keys email terms requestDomains challengeDir csrData 144 certificate <- certify directoryUrl keys ((,) terms <$> email) (fileProvisioner challengeDir) certReq
144 145
145 either (error . ("Error: " ++)) (LC.writeFile domainCertFile) certificate 146 either (error . ("Error: " ++)) (LC.writeFile domainCertFile) certificate
146 147
diff --git a/src/Network/ACME.hs b/src/Network/ACME.hs
index 4df9f9b..5a66028 100644
--- a/src/Network/ACME.hs
+++ b/src/Network/ACME.hs
@@ -37,21 +37,38 @@ import Text.Email.Validate
37import Text.Domain.Validate hiding (validate) 37import Text.Domain.Validate hiding (validate)
38import Network.URI 38import Network.URI
39 39
40certify :: URI -> Keys -> Maybe EmailAddress -> URI -> [DomainName] -> WritableDir -> CSR -> IO (Either String LC.ByteString) 40type HttpProvisioner = URI -> ByteString -> IO ()
41certify directoryUrl keys optEmail terms requestDomains optChallengeDir csrData = 41
42fileProvisioner :: WritableDir -> HttpProvisioner
43fileProvisioner challengeDir = BC.writeFile . uToF
44 where
45 uToF = (coerce challengeDir </>) . takeWhileEnd (/= '/') . uriPath
46 takeWhileEnd f = reverse . takeWhile f . reverse
47
48acmeChallengeURI :: DomainName -> BC.ByteString -> URI
49acmeChallengeURI dom tok = URI
50 "http:"
51 (Just $ URIAuth "" (domainToString dom) "")
52 ("/.well-known/acme-challenge" </> BC.unpack tok)
53 ""
54 ""
55
56certify :: URI -> Keys -> Maybe (URI, EmailAddress) -> HttpProvisioner -> CSR -> IO (Either String LC.ByteString)
57certify directoryUrl keys reg provision certReq =
42 58
43 runACME directoryUrl keys $ do 59 runACME directoryUrl keys $ do
44 forM_ optEmail $ register terms >=> statusReport 60 forM_ reg $ uncurry register >=> statusReport
45 61
46 let performChallenge (ChallengeRequest nextUri token thumbtoken) = do 62 let performChallenge domain (ChallengeRequest nextUri token thumbtoken) = do
47 liftIO $ BC.writeFile (coerce optChallengeDir </> BC.unpack token) thumbtoken 63 liftIO $ provision (acmeChallengeURI domain token) thumbtoken
48 notifyChallenge nextUri thumbtoken >>= statusReport >>= ncErrorReport 64 notifyChallenge nextUri thumbtoken >>= statusReport >>= ncErrorReport
49 65
50 challengeResultLinks <- forM requestDomains $ challengeRequest >=> statusReport >=> extractCR >=> performChallenge 66 challengeResultLinks <- forM (csrDomains certReq) $ \dom ->
67 challengeRequest dom >>= statusReport >>= extractCR >>= performChallenge dom
51 68
52 pollResults challengeResultLinks >>= 69 pollResults challengeResultLinks >>=
53 either (return . Left . ("certificate receipt was not attempted because a challenge failed: " ++)) 70 either (return . Left . ("certificate receipt was not attempted because a challenge failed: " ++))
54 (const (retrieveCert csrData >>= statusReport <&> checkCertResponse)) 71 (const (retrieveCert certReq >>= statusReport <&> checkCertResponse))
55 72
56pollResults :: [Response LC.ByteString] -> ACME (Either String ()) 73pollResults :: [Response LC.ByteString] -> ACME (Either String ())
57pollResults [] = return $ Right () 74pollResults [] = return $ Right ()
@@ -68,7 +85,7 @@ pollResults (link:links) = do
68 85
69data ChallengeRequest = ChallengeRequest { crUri :: String, crToken :: ByteString, crThumbToken :: ByteString } 86data ChallengeRequest = ChallengeRequest { crUri :: String, crToken :: ByteString, crThumbToken :: ByteString }
70 87
71newtype CSR = CSR ByteString 88data CSR = CSR { csrDomains :: [DomainName], csrData :: ByteString }
72 89
73newtype WritableDir = WritableDir String 90newtype WritableDir = WritableDir String
74ensureWritableDir :: FilePath -> String -> IO WritableDir 91ensureWritableDir :: FilePath -> String -> IO WritableDir
@@ -137,7 +154,7 @@ checkCertResponse r =
137 isSuccess n = n >= 200 && n <= 300 154 isSuccess n = n >= 200 && n <= 300
138 155
139retrieveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => CSR -> m (Response LC.ByteString) 156retrieveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => CSR -> m (Response LC.ByteString)
140retrieveCert input = sendPayload _newCert (csr $ coerce input) 157retrieveCert input = sendPayload _newCert (csr $ csrData input)
141 158
142notifyChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString) 159notifyChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString)
143notifyChallenge uri thumbtoken = sendPayload (const uri) (challenge thumbtoken) 160notifyChallenge uri thumbtoken = sendPayload (const uri) (challenge thumbtoken)