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 <Directory>
-- 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
        [ "<IfVersion < 2.4>"
        , "Order allow,deny"
        , "allow from all"
        , "</IfVersion>"
        , "<IfVersion >= 2.4>"
        , "Require all granted"
        , "</IfVersion>"
        ]

-- | Config file fragment that can be inserted into a <VirtualHost>
-- stanza to allow apache to display directory index icons.
iconDir :: ConfigLine
iconDir = unlines
        [ "<Directory \"/usr/share/apache2/icons\">"
        , "Options Indexes MultiViews"
        , "AllowOverride None"
        , allowAll
        , "  </Directory>"
        ]

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 $
        [ "<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>"
        ]

-- | 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 =
                [ "<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>"
                ]