1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9
10 inherit (lib)
11 generators
12 mapAttrs
13 mkDefault
14 mkEnableOption
15 mkIf
16 mkPackageOption
17 mkOption
18 types
19 ;
20
21 cfg = config.services.grav;
22
23 yamlFormat = pkgs.formats.yaml { };
24
25 poolName = "grav";
26
27 servedRoot = pkgs.runCommand "grav-served-root" { } ''
28 cp --reflink=auto --no-preserve=mode -r ${cfg.package} $out
29
30 for p in assets images user system/config; do
31 rm -rf $out/$p
32 ln -sf /var/lib/grav/$p $out/$p
33 done
34 '';
35
36 systemSettingsYaml = yamlFormat.generate "grav-settings.yaml" cfg.systemSettings;
37
38in
39{
40 options.services.grav = {
41 enable = mkEnableOption "grav";
42
43 package = mkPackageOption pkgs "grav" { };
44
45 root = mkOption {
46 type = types.path;
47 default = "/var/lib/grav";
48 description = ''
49 Root of the application.
50 '';
51 };
52
53 pool = mkOption {
54 type = types.str;
55 default = "${poolName}";
56 description = ''
57 Name of existing phpfpm pool that is used to run web-application.
58 If not specified a pool will be created automatically with
59 default values.
60 '';
61 };
62
63 virtualHost = mkOption {
64 type = types.nullOr types.str;
65 default = "grav";
66 description = ''
67 Name of the nginx virtualhost to use and setup. If null, do not setup
68 any virtualhost.
69 '';
70 };
71
72 phpPackage = mkPackageOption pkgs "php" { };
73
74 maxUploadSize = mkOption {
75 type = types.str;
76 default = "128M";
77 description = ''
78 The upload limit for files. This changes the relevant options in
79 {file}`php.ini` and nginx if enabled.
80 '';
81 };
82
83 systemSettings = mkOption {
84 type = yamlFormat.type;
85 default = {
86 log = {
87 handler = "syslog";
88 };
89 };
90 description = ''
91 Settings written to {file}`user/config/system.yaml`.
92 '';
93 };
94 };
95
96 config = mkIf cfg.enable {
97 services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
98 ${poolName} = {
99 user = "grav";
100 group = "grav";
101
102 phpPackage = cfg.phpPackage.buildEnv {
103 extensions =
104 { all, enabled }:
105 enabled
106 ++ (with all; [
107 apcu
108 xml
109 yaml
110 ]);
111
112 extraConfig = generators.toKeyValue { mkKeyValue = generators.mkKeyValueDefault { } " = "; } {
113 output_buffering = "0";
114 short_open_tag = "Off";
115 expose_php = "Off";
116 error_reporting = "E_ALL";
117 display_errors = "stderr";
118 "opcache.interned_strings_buffer" = "8";
119 "opcache.max_accelerated_files" = "10000";
120 "opcache.memory_consumption" = "128";
121 "opcache.revalidate_freq" = "1";
122 "opcache.fast_shutdown" = "1";
123 "openssl.cafile" = config.security.pki.caBundle;
124 catch_workers_output = "yes";
125
126 upload_max_filesize = cfg.maxUploadSize;
127 post_max_size = cfg.maxUploadSize;
128 memory_limit = cfg.maxUploadSize;
129 "apc.enable_cli" = "1";
130 };
131 };
132
133 phpEnv = {
134 GRAV_ROOT = toString servedRoot;
135 GRAV_SYSTEM_PATH = "${servedRoot}/system";
136 GRAV_CACHE_PATH = "/var/cache/grav";
137 GRAV_BACKUP_PATH = "/var/lib/grav/backup";
138 GRAV_LOG_PATH = "/var/log/grav";
139 GRAV_TMP_PATH = "/var/tmp/grav";
140 };
141
142 settings = mapAttrs (name: mkDefault) {
143 "listen.owner" = config.services.nginx.user;
144 "listen.group" = config.services.nginx.group;
145 "listen.mode" = "0600";
146 "pm" = "dynamic";
147 "pm.max_children" = 75;
148 "pm.start_servers" = 10;
149 "pm.min_spare_servers" = 5;
150 "pm.max_spare_servers" = 20;
151 "pm.max_requests" = 500;
152 "catch_workers_output" = 1;
153 };
154 };
155 };
156
157 services.nginx = mkIf (cfg.virtualHost != null) {
158 enable = true;
159 virtualHosts = {
160 ${cfg.virtualHost} = {
161 root = "${servedRoot}";
162
163 locations = {
164 "= /robots.txt" = {
165 priority = 100;
166 extraConfig = ''
167 allow all;
168 access_log off;
169 '';
170 };
171
172 "~ \\.php$" = {
173 priority = 200;
174 extraConfig = ''
175 fastcgi_split_path_info ^(.+\.php)(/.+)$;
176 fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket};
177 fastcgi_index index.php;
178 '';
179 };
180
181 "~* /(\\.git|cache|bin|logs|backup|tests)/.*$" = {
182 priority = 300;
183 extraConfig = ''
184 return 403;
185 '';
186 };
187
188 # deny running scripts inside core system folders
189 "~* /(system|vendor)/.*\\.(txt|xml|md|html|htm|shtml|shtm|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$" =
190 {
191 priority = 300;
192 extraConfig = ''
193 return 403;
194 '';
195 };
196
197 # deny running scripts inside user folder
198 "~* /user/.*\\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$" = {
199 priority = 300;
200 extraConfig = ''
201 return 403;
202 '';
203 };
204
205 # deny access to specific files in the root folder
206 "~ /(LICENSE\\.txt|composer\\.lock|composer\\.json|nginx\\.conf|web\\.config|htaccess\\.txt|\\.htaccess)" =
207 {
208 priority = 300;
209 extraConfig = ''
210 return 403;
211 '';
212 };
213
214 # deny all files and folder beginning with a dot (hidden files & folders)
215 "~ (^|/)\\." = {
216 priority = 300;
217 extraConfig = ''
218 return 403;
219 '';
220 };
221
222 "/" = {
223 priority = 400;
224 index = "index.php";
225 extraConfig = ''
226 try_files $uri $uri/ /index.php?$query_string;
227 '';
228 };
229 };
230
231 extraConfig = ''
232 index index.php index.html /index.php$request_uri;
233 add_header X-Content-Type-Options nosniff;
234 add_header X-XSS-Protection "1; mode=block";
235 add_header X-Download-Options noopen;
236 add_header X-Permitted-Cross-Domain-Policies none;
237 add_header X-Frame-Options sameorigin;
238 add_header Referrer-Policy no-referrer;
239 client_max_body_size ${cfg.maxUploadSize};
240 fastcgi_buffers 64 4K;
241 fastcgi_hide_header X-Powered-By;
242 gzip on;
243 gzip_vary on;
244 gzip_comp_level 4;
245 gzip_min_length 256;
246 gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
247 gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
248 '';
249 };
250 };
251 };
252
253 systemd.tmpfiles.rules =
254 let
255 datadir = "/var/lib/grav";
256 in
257 map (dir: "d '${dir}' 0750 grav grav - -") [
258 "/var/cache/grav"
259 "${datadir}/assets"
260 "${datadir}/backup"
261 "${datadir}/images"
262 "${datadir}/system/config"
263 "${datadir}/user/accounts"
264 "${datadir}/user/config"
265 "${datadir}/user/data"
266 "/var/log/grav"
267 ]
268 ++ [ "L+ ${datadir}/user/config/system.yaml - - - - ${systemSettingsYaml}" ];
269
270 systemd.services = {
271 "phpfpm-${poolName}" = mkIf (cfg.pool == "${poolName}") {
272 restartTriggers = [
273 servedRoot
274 systemSettingsYaml
275 ];
276
277 serviceConfig = {
278 ExecStartPre = pkgs.writeShellScript "grav-pre-start" ''
279 function setPermits() {
280 chmod -R o-rx "$1"
281 chown -R grav:grav "$1"
282 }
283
284 tmpDir=/var/tmp/grav
285 dataDir=/var/lib/grav
286
287 mkdir $tmpDir
288 setPermits $tmpDir
289
290 for path in config/site.yaml pages plugins themes; do
291 fullPath="$dataDir/user/$path"
292 if [[ ! -e $fullPath ]]; then
293 cp --reflink=auto --no-preserve=mode -r \
294 ${cfg.package}/user/$path $fullPath
295 fi
296 setPermits $fullPath
297 done
298
299 systemConfigDir=$dataDir/system/config
300 if [[ ! -e $systemConfigDir/system.yaml ]]; then
301 cp --reflink=auto --no-preserve=mode -r \
302 ${cfg.package}/system/config/* $systemConfigDir/
303 fi
304 setPermits $systemConfigDir
305 '';
306 };
307 };
308 };
309
310 users.users.grav = {
311 isSystemUser = true;
312 description = "Grav service user";
313 home = "/var/lib/grav";
314 group = "grav";
315 };
316
317 users.groups.grav = {
318 members = [ config.services.nginx.user ];
319 };
320 };
321}