diff options
author | Andrew Cady <d@jerkface.net> | 2016-04-09 17:50:20 -0400 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2016-04-09 17:50:20 -0400 |
commit | 6224e4d1dc99244fbad13dc0613fa16e87c20396 (patch) | |
tree | b68484de3c49d402532438b02b1c5b758c7aad8d | |
parent | 9bf06242513945fbadd6fcd20b41efd0f1b073c2 (diff) |
generate stub CertSpec objects from config file
(CertSpec is the new name for AcmeCertRequest)
-rw-r--r-- | acme-certify.hs | 123 |
1 files changed, 81 insertions, 42 deletions
diff --git a/acme-certify.hs b/acme-certify.hs index 48901c9..579622f 100644 --- a/acme-certify.hs +++ b/acme-certify.hs | |||
@@ -1,9 +1,11 @@ | |||
1 | {-# LANGUAGE FlexibleContexts #-} | 1 | {-# LANGUAGE FlexibleContexts #-} |
2 | {-# LANGUAGE NamedFieldPuns #-} | 2 | {-# LANGUAGE FlexibleInstances #-} |
3 | {-# LANGUAGE NoImplicitPrelude #-} | 3 | {-# LANGUAGE NamedFieldPuns #-} |
4 | {-# LANGUAGE OverloadedStrings #-} | 4 | {-# LANGUAGE NoImplicitPrelude #-} |
5 | {-# LANGUAGE RecordWildCards #-} | 5 | {-# LANGUAGE OverloadedStrings #-} |
6 | {-# LANGUAGE ScopedTypeVariables #-} | 6 | {-# LANGUAGE RecordWildCards #-} |
7 | {-# LANGUAGE ScopedTypeVariables #-} | ||
8 | {-# LANGUAGE TypeSynonymInstances #-} | ||
7 | 9 | ||
8 | -------------------------------------------------------------------------------- | 10 | -------------------------------------------------------------------------------- |
9 | -- | Get a certificate from Let's Encrypt using the ACME protocol. | 11 | -- | Get a certificate from Let's Encrypt using the ACME protocol. |
@@ -16,7 +18,7 @@ import BasePrelude | |||
16 | import Control.Lens hiding ((&)) | 18 | import Control.Lens hiding ((&)) |
17 | import Data.Aeson.Lens | 19 | import Data.Aeson.Lens |
18 | import qualified Data.HashMap.Strict as HashMap | 20 | import qualified Data.HashMap.Strict as HashMap |
19 | import Data.Text (unpack) | 21 | import Data.Text (Text, unpack) |
20 | import Data.Yaml (Object) | 22 | import Data.Yaml (Object) |
21 | import qualified Data.Yaml.Config as Config | 23 | import qualified Data.Yaml.Config as Config |
22 | import Data.Yaml.Config.Internal (Config (..)) | 24 | import Data.Yaml.Config.Internal (Config (..)) |
@@ -83,12 +85,17 @@ data UpdateOpts = UpdateOpts { | |||
83 | updateConfigFile :: Maybe FilePath | 85 | updateConfigFile :: Maybe FilePath |
84 | } | 86 | } |
85 | 87 | ||
86 | data AcmeCertRequest = AcmeCertRequest { | 88 | instance Show HttpProvisioner where |
87 | acrDomains :: [(DomainName, HttpProvisioner)], | 89 | show _ = "<code>" |
88 | acrSkipDH :: Bool, | 90 | instance Show Keys where |
89 | acrCertificateDir :: FilePath, | 91 | show _ = "<keys>" |
90 | acrUserKeys :: Keys | 92 | |
91 | } | 93 | data CertSpec = CertSpec { |
94 | csDomains :: [(DomainName, HttpProvisioner)], | ||
95 | csSkipDH :: Bool, | ||
96 | csCertificateDir :: FilePath, | ||
97 | csUserKeys :: Keys | ||
98 | } deriving Show | ||
92 | 99 | ||
93 | updateOpts :: Parser Command | 100 | updateOpts :: Parser Command |
94 | updateOpts = fmap Update $ | 101 | updateOpts = fmap Update $ |
@@ -148,16 +155,48 @@ extractObject (Config _ o) = o | |||
148 | runUpdate :: UpdateOpts -> IO () | 155 | runUpdate :: UpdateOpts -> IO () |
149 | runUpdate UpdateOpts { .. } = do | 156 | runUpdate UpdateOpts { .. } = do |
150 | config <- Config.load "config.yaml" | 157 | config <- Config.load "config.yaml" |
151 | hosts <- Config.subconfig "hosts" config | 158 | hostsConfig <- Config.subconfig "hosts" config |
152 | forM_ (Config.keys hosts) $ \host -> do | 159 | certReqDomains <- fmap concat <$> forM (Config.keys hostsConfig) $ \host -> |
153 | domains <- extractObject <$> (Config.subconfig host hosts >>= Config.subconfig "domains") | 160 | do |
154 | putStrLn $ unpack host ++ ": " | 161 | hostParts <- (Config.subconfig host hostsConfig >>= Config.subconfig |
155 | forM_ (HashMap.keys domains) $ \domain -> do | 162 | "domains") <&> extractObject |
156 | let subdomains = map (<..> unpack domain) $ sort $ concat $ HashMap.lookup domain domains & toListOf (_Just . _String . to (words . unpack)) | 163 | forM (HashMap.keys hostParts) $ \domain -> |
157 | putStrLn $ " " ++ unwords subdomains | 164 | return (unpack host, combineSubdomains domain hostParts) |
158 | putStrLn "" | 165 | forM_ certReqDomains print |
166 | |||
167 | globalCertificateDir <- getHomeDirectory <&> (</> ".acme/test") | ||
168 | createDirectoryIfMissing True globalCertificateDir | ||
169 | |||
170 | Just keys <- getOrCreateKeys $ globalCertificateDir </> "rsa.key" | ||
171 | |||
172 | |||
173 | certSpecs :: [CertSpec] <- forM certReqDomains $ \(host, domains) -> do | ||
174 | provisioners <- mapM (chooseProvisioner host) domains | ||
175 | return $ certSpec globalCertificateDir keys (host, provisioners) | ||
176 | |||
177 | mapM_ print certSpecs | ||
159 | error "Error: unimplemented" | 178 | error "Error: unimplemented" |
160 | 179 | ||
180 | where | ||
181 | chooseProvisioner :: String -> String -> IO (DomainName, HttpProvisioner) | ||
182 | chooseProvisioner host domain = do -- TODO: implement | ||
183 | let errmsg = "whatever" | ||
184 | dir <- ensureWritableDir "/var/www/html/.well-known/acme-challenge/" errmsg | ||
185 | return (domainName' domain, provisionViaFile dir) | ||
186 | |||
187 | certSpec :: FilePath -> Keys -> (String, [(DomainName, HttpProvisioner)]) -> CertSpec | ||
188 | certSpec baseDir keys (host, requestDomains) = CertSpec { .. } | ||
189 | where | ||
190 | csDomains = requestDomains | ||
191 | csSkipDH = True -- TODO: implement | ||
192 | csUserKeys = keys | ||
193 | csCertificateDir = baseDir </> host </> (show . fst) (head requestDomains) | ||
194 | |||
195 | combineSubdomains :: AsPrimitive v => Text -> HashMap.HashMap Text v -> [String] | ||
196 | combineSubdomains domain subs = | ||
197 | map (<..> unpack domain) $ sort -- relying on the fact that '.' sorts first | ||
198 | $ concat $ HashMap.lookup domain subs & toListOf (_Just . _String . to (words . unpack)) | ||
199 | |||
161 | runCertify :: CertifyOpts -> IO (Either String ()) | 200 | runCertify :: CertifyOpts -> IO (Either String ()) |
162 | runCertify CertifyOpts{..} = do | 201 | runCertify CertifyOpts{..} = do |
163 | let terms = fromMaybe defaultTerms (join $ parseAbsoluteURI <$> optTerms) | 202 | let terms = fromMaybe defaultTerms (join $ parseAbsoluteURI <$> optTerms) |
@@ -170,45 +209,45 @@ runCertify CertifyOpts{..} = do | |||
170 | issuerCert <- readX509 letsEncryptX1CrossSigned | 209 | issuerCert <- readX509 letsEncryptX1CrossSigned |
171 | 210 | ||
172 | seq email (return ()) | 211 | seq email (return ()) |
173 | doesDirectoryExist domainDir `otherwiseM` createDirectory domainDir | 212 | createDirectoryIfMissing False domainDir |
174 | challengeDir <- ensureWritableDir optChallengeDir "challenge directory" | 213 | challengeDir <- ensureWritableDir optChallengeDir "challenge directory" |
175 | void $ ensureWritableDir domainDir "domain directory" | 214 | void $ ensureWritableDir domainDir "domain directory" |
176 | 215 | ||
177 | Just keys <- getOrCreateKeys privKeyFile | 216 | Just keys <- getOrCreateKeys privKeyFile |
178 | 217 | ||
179 | let req = AcmeCertRequest {..} | 218 | let req = CertSpec {..} |
180 | acrDomains = map (flip (,) (provisionViaFile challengeDir)) requestDomains | 219 | csDomains = map (flip (,) (provisionViaFile challengeDir)) requestDomains |
181 | acrSkipDH = optSkipDH | 220 | csSkipDH = optSkipDH |
182 | acrUserKeys = keys | 221 | csUserKeys = keys |
183 | acrCertificateDir = domainDir | 222 | csCertificateDir = domainDir |
184 | 223 | ||
185 | unless optSkipProvisionCheck $ | 224 | unless optSkipProvisionCheck $ |
186 | forM_ acrDomains $ uncurry canProvision >=> | 225 | forM_ csDomains $ uncurry canProvision >=> |
187 | (`unless` error "Error: cannot provision files to web server") | 226 | (`unless` error "Error: cannot provision files to web server") |
188 | 227 | ||
189 | go' directoryUrl terms email issuerCert req | 228 | go' directoryUrl terms email issuerCert req |
190 | 229 | ||
191 | go' :: URI -> URI -> Maybe EmailAddress -> X509 -> AcmeCertRequest -> IO (Either String ()) | 230 | go' :: URI -> URI -> Maybe EmailAddress -> X509 -> CertSpec -> IO (Either String ()) |
192 | go' directoryUrl terms email issuerCert acr@AcmeCertRequest{..} = do | 231 | go' directoryUrl terms email issuerCert cs@CertSpec{..} = do |
193 | Just domainKeys <- getOrCreateKeys $ acrCertificateDir </> "rsa.key" | 232 | Just domainKeys <- getOrCreateKeys $ csCertificateDir </> "rsa.key" |
194 | dh <- saveDhParams acr | 233 | dh <- saveDhParams cs |
195 | 234 | ||
196 | certificate <- certify directoryUrl acrUserKeys ((,) terms <$> email) domainKeys acrDomains | 235 | certificate <- certify directoryUrl csUserKeys ((,) terms <$> email) domainKeys csDomains |
197 | for certificate $ saveCertificate issuerCert dh domainKeys acr | 236 | for certificate $ saveCertificate issuerCert dh domainKeys cs |
198 | 237 | ||
199 | saveDhParams :: AcmeCertRequest -> IO (Maybe DHP) | 238 | saveDhParams :: CertSpec -> IO (Maybe DHP) |
200 | saveDhParams AcmeCertRequest{acrSkipDH, acrCertificateDir} = do | 239 | saveDhParams CertSpec{csSkipDH, csCertificateDir} = do |
201 | let domainDhFile = acrCertificateDir </> "dhparams.pem" | 240 | let domainDhFile = csCertificateDir </> "dhparams.pem" |
202 | if acrSkipDH then return Nothing else Just <$> getOrCreateDH domainDhFile | 241 | if csSkipDH then return Nothing else Just <$> getOrCreateDH domainDhFile |
203 | 242 | ||
204 | saveCertificate :: X509 -> Maybe DHP -> Keys -> AcmeCertRequest -> X509 -> IO () | 243 | saveCertificate :: X509 -> Maybe DHP -> Keys -> CertSpec -> X509 -> IO () |
205 | saveCertificate issuerCert dh domainKeys AcmeCertRequest{acrCertificateDir} = saveBoth | 244 | saveCertificate issuerCert dh domainKeys CertSpec{csCertificateDir} = saveBoth |
206 | where | 245 | where |
207 | saveBoth x509 = savePEM x509 >> saveCombined x509 | 246 | saveBoth x509 = savePEM x509 >> saveCombined x509 |
208 | saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile domainCombinedFile | 247 | saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile domainCombinedFile |
209 | savePEM = writeX509 >=> writeFile domainCertFile | 248 | savePEM = writeX509 >=> writeFile domainCertFile |
210 | domainCombinedFile = acrCertificateDir </> "cert.combined.pem" | 249 | domainCombinedFile = csCertificateDir </> "cert.combined.pem" |
211 | domainCertFile = acrCertificateDir </> "cert.pem" | 250 | domainCertFile = csCertificateDir </> "cert.pem" |
212 | 251 | ||
213 | genKey :: IO String | 252 | genKey :: IO String |
214 | genKey = withOpenSSL $ do | 253 | genKey = withOpenSSL $ do |