summaryrefslogtreecommitdiff
path: root/src/Propellor/Property/Apache.hs
blob: 854d0eaab8d4c090b4a9fdb7752e56bbc6dcf9e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
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>"
		]