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] -- | Control whether an apache configuration file is enabled. -- -- The String is the base name of the configuration, eg "charset" or "gitweb". 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] -- | Make apache listen on the specified ports. -- -- Note that ports are also specified inside a site's config file, -- so that also needs to be changed. listenPorts :: [Port] -> Property DebianLike listenPorts ps = "/etc/apache2/ports.conf" `File.hasContent` map portline ps `onChange` restarted where portline port = "Listen " ++ val port -- This is a list of config files because different versions of apache -- use different filenames. Propellor simply writes them all. siteCfg :: Domain -> [FilePath] siteCfg domain = -- Debian pre-2.4 [ "/etc/apache2/sites-available/" ++ domain -- Debian 2.4+ , "/etc/apache2/sites-available/" ++ domain ++ ".conf" ] -- | Configure apache to use SNI to differentiate between -- https hosts. -- -- This was off by default in apache 2.2.22. Newver versions enable -- it by default. This property uses the filename used by the old version. 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 -- | Config file fragment that can be inserted into a -- stanza to allow global read access to the directory. -- -- Works with multiple versions of apache that have different ways to do -- it. allowAll :: ConfigLine allowAll = unlines [ "" , "Order allow,deny" , "allow from all" , "" , "= 2.4>" , "Require all granted" , "" ] -- | Config file fragment that can be inserted into a -- stanza to allow apache to display directory index icons. iconDir :: ConfigLine iconDir = unlines [ "" , "Options Indexes MultiViews" , "AllowOverride None" , allowAll , " " ] type WebRoot = FilePath -- | A basic virtual host, publishing a directory, and logging to -- the combined apache log file. Not https capable. virtualHost :: Domain -> Port -> WebRoot -> RevertableProperty DebianLike DebianLike virtualHost domain port docroot = virtualHost' domain port docroot [] -- | Like `virtualHost` but with additional config lines added. virtualHost' :: Domain -> Port -> WebRoot -> [ConfigLine] -> RevertableProperty DebianLike DebianLike virtualHost' domain port docroot addedcfg = siteEnabled domain $ [ "" , "ServerName " ++ domain ++ ":" ++ val port , "DocumentRoot " ++ docroot , "ErrorLog /var/log/apache2/error.log" , "LogLevel warn" , "CustomLog /var/log/apache2/access.log combined" , "ServerSignature On" ] ++ addedcfg ++ [ "" ] -- | A virtual host using https, with the certificate obtained -- using `Propellor.Property.LetsEncrypt.letsEncrypt`. -- -- http connections are redirected to https. -- -- Example: -- -- > httpsVirtualHost "example.com" "/var/www" -- > (LetsEncrypt.AgreeTOS (Just "me@my.domain")) -- -- Note that reverting this property does not remove the certificate from -- letsencrypt's cert store. httpsVirtualHost :: Domain -> WebRoot -> LetsEncrypt.AgreeTOS -> RevertableProperty DebianLike DebianLike httpsVirtualHost domain docroot letos = httpsVirtualHost' domain docroot letos [] -- | Like `httpsVirtualHost` but with additional config lines added. 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 $ -- The sslconffile is only created after letsencrypt gets -- the cert. The "*" is needed to make apache not error -- when the file doesn't exist. ("IncludeOptional " ++ sslconffile "*") : vhost (Port 80) [ "RewriteEngine On" -- Pass through .well-known directory on http for the -- letsencrypt acme challenge. , "RewriteRule ^/.well-known/(.*) - [L]" -- Everything else redirects to https , "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 -- always reload since the cert has changed & 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 = [ "" , "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 ++ [ "" ]