module Propellor.Property.Apache where
import Propellor.Base
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.Service as Service
import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
installed :: Property DebianLike
installed = Apt.installed ["apache2"]
restarted :: Property DebianLike
restarted = Service.restarted "apache2"
reloaded :: Property DebianLike
reloaded = Service.reloaded "apache2"
type ConfigLine = String
type ConfigFile = [ConfigLine]
siteEnabled :: Domain -> ConfigFile -> RevertableProperty DebianLike DebianLike
siteEnabled domain cf = siteEnabled' domain cf <!> siteDisabled domain
siteEnabled' :: Domain -> ConfigFile -> Property DebianLike
siteEnabled' domain cf = combineProperties ("apache site enabled " ++ domain) $ props
& siteAvailable domain cf
`requires` installed
`onChange` reloaded
& check (not <$> isenabled)
(cmdProperty "a2ensite" ["--quiet", domain])
`requires` installed
`onChange` reloaded
where
isenabled = boolSystem "a2query" [Param "-q", Param "-s", Param domain]
siteDisabled :: Domain -> Property DebianLike
siteDisabled domain = combineProperties
("apache site disabled " ++ domain)
(toProps $ map File.notPresent (siteCfg domain))
`onChange` (cmdProperty "a2dissite" ["--quiet", domain] `assume` MadeChange)
`requires` installed
`onChange` reloaded
siteAvailable :: Domain -> ConfigFile -> Property DebianLike
siteAvailable domain cf = combineProperties ("apache site available " ++ domain) $
toProps $ map tightenTargets $
map (`File.hasContent` (comment:cf)) (siteCfg domain)
where
comment = "# deployed with propellor, do not modify"
modEnabled :: String -> RevertableProperty DebianLike DebianLike
modEnabled modname = enable <!> disable
where
enable = check (not <$> isenabled)
(cmdProperty "a2enmod" ["--quiet", modname])
`describe` ("apache module enabled " ++ modname)
`requires` installed
`onChange` reloaded
disable = check isenabled
(cmdProperty "a2dismod" ["--quiet", modname])
`describe` ("apache module disabled " ++ modname)
`requires` installed
`onChange` reloaded
isenabled = boolSystem "a2query" [Param "-q", Param "-m", Param modname]
confEnabled :: String -> RevertableProperty DebianLike DebianLike
confEnabled confname = enable <!> disable
where
enable = check (not <$> isenabled)
(cmdProperty "a2enconf" ["--quiet", confname])
`describe` ("apache configuration enabled " ++ confname)
`requires` installed
`onChange` reloaded
disable = check isenabled
(cmdProperty "a2disconf" ["--quiet", confname])
`describe` ("apache configuration disabled " ++ confname)
`requires` installed
`onChange` reloaded
isenabled = boolSystem "a2query" [Param "-q", Param "-c", Param confname]
listenPorts :: [Port] -> Property DebianLike
listenPorts ps = "/etc/apache2/ports.conf" `File.hasContent` map portline ps
`onChange` restarted
where
portline port = "Listen " ++ val port
siteCfg :: Domain -> [FilePath]
siteCfg domain =
[ "/etc/apache2/sites-available/" ++ domain
, "/etc/apache2/sites-available/" ++ domain ++ ".conf"
]
multiSSL :: Property DebianLike
multiSSL = check (doesDirectoryExist "/etc/apache2/conf.d") $
"/etc/apache2/conf.d/ssl" `File.hasContent`
[ "NameVirtualHost *:443"
, "SSLStrictSNIVHostCheck off"
]
`describe` "apache SNI enabled"
`onChange` reloaded
allowAll :: ConfigLine
allowAll = unlines
[ "<IfVersion < 2.4>"
, "Order allow,deny"
, "allow from all"
, "</IfVersion>"
, "<IfVersion >= 2.4>"
, "Require all granted"
, "</IfVersion>"
]
iconDir :: ConfigLine
iconDir = unlines
[ "<Directory \"/usr/share/apache2/icons\">"
, "Options Indexes MultiViews"
, "AllowOverride None"
, allowAll
, " </Directory>"
]
type WebRoot = FilePath
virtualHost :: Domain -> Port -> WebRoot -> RevertableProperty DebianLike DebianLike
virtualHost domain port docroot = virtualHost' domain port docroot []
virtualHost' :: Domain -> Port -> WebRoot -> [ConfigLine] -> RevertableProperty DebianLike DebianLike
virtualHost' domain port docroot addedcfg = siteEnabled domain $
[ "<VirtualHost *:" ++ val port ++ ">"
, "ServerName " ++ domain ++ ":" ++ val port
, "DocumentRoot " ++ docroot
, "ErrorLog /var/log/apache2/error.log"
, "LogLevel warn"
, "CustomLog /var/log/apache2/access.log combined"
, "ServerSignature On"
]
++ addedcfg ++
[ "</VirtualHost>"
]
httpsVirtualHost :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> RevertableProperty DebianLike DebianLike
httpsVirtualHost domain docroot letos = httpsVirtualHost' domain docroot letos []
httpsVirtualHost' :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> [ConfigLine] -> RevertableProperty DebianLike DebianLike
httpsVirtualHost' domain docroot letos addedcfg = setup <!> teardown
where
setup = setuphttp
`requires` modEnabled "rewrite"
`requires` modEnabled "ssl"
`before` setuphttps
teardown = siteDisabled domain
setuphttp = (siteEnabled' domain $
("IncludeOptional " ++ sslconffile "*")
: vhost (Port 80)
[ "RewriteEngine On"
, "RewriteRule ^/.well-known/(.*) - [L]"
, "RewriteRule ^/(.*) https://" ++ domain ++ "/$1 [L,R,NE]"
])
`requires` File.dirExists (takeDirectory cf)
setuphttps = LetsEncrypt.letsEncrypt letos domain docroot
`onChange` postsetuphttps
postsetuphttps = combineProperties (domain ++ " ssl cert installed") $ props
& File.hasContent cf sslvhost
`onChange` reloaded
& reloaded
where
sslvhost = vhost (Port 443)
[ "SSLEngine on"
, "SSLCertificateFile " ++ LetsEncrypt.certFile domain
, "SSLCertificateKeyFile " ++ LetsEncrypt.privKeyFile domain
, "SSLCertificateChainFile " ++ LetsEncrypt.chainFile domain
]
cf = sslconffile "letsencrypt"
sslconffile s = "/etc/apache2/sites-available/ssl/" ++ domain ++ "/" ++ s ++ ".conf"
vhost p ls =
[ "<VirtualHost *:" ++ val p ++">"
, "ServerName " ++ domain ++ ":" ++ val p
, "DocumentRoot " ++ docroot
, "ErrorLog /var/log/apache2/error.log"
, "LogLevel warn"
, "CustomLog /var/log/apache2/access.log combined"
, "ServerSignature On"
] ++ ls ++ addedcfg ++
[ "</VirtualHost>"
]