diff options
Diffstat (limited to 'modules')
-rw-r--r-- | modules/default.nix | 13 | ||||
-rw-r--r-- | modules/myids.nix | 22 | ||||
-rw-r--r-- | modules/secrets.nix | 61 | ||||
-rw-r--r-- | modules/webapps/diaspora.nix | 171 | ||||
-rw-r--r-- | modules/webapps/etherpad-lite.nix | 158 | ||||
-rw-r--r-- | modules/webapps/mastodon.nix | 223 | ||||
-rw-r--r-- | modules/webapps/mediagoblin.nix | 237 | ||||
-rw-r--r-- | modules/webapps/peertube.nix | 105 | ||||
-rw-r--r-- | modules/webapps/webstats/default.nix | 81 | ||||
-rw-r--r-- | modules/webapps/webstats/goaccess.conf | 99 | ||||
-rw-r--r-- | modules/websites/default.nix | 199 | ||||
-rw-r--r-- | modules/websites/httpd-service-builder.nix | 746 | ||||
-rw-r--r-- | modules/websites/nosslVhost/index.html | 11 |
13 files changed, 2126 insertions, 0 deletions
diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 00000000..acb0bb51 --- /dev/null +++ b/modules/default.nix | |||
@@ -0,0 +1,13 @@ | |||
1 | { | ||
2 | myids = ./myids.nix; | ||
3 | secrets = ./secrets.nix; | ||
4 | |||
5 | webstats = ./webapps/webstats; | ||
6 | diaspora = ./webapps/diaspora.nix; | ||
7 | etherpad-lite = ./webapps/etherpad-lite.nix; | ||
8 | mastodon = ./webapps/mastodon.nix; | ||
9 | mediagoblin = ./webapps/mediagoblin.nix; | ||
10 | peertube = ./webapps/peertube.nix; | ||
11 | |||
12 | websites = ./websites; | ||
13 | } // (if builtins.pathExists ./private then import ./private else {}) | ||
diff --git a/modules/myids.nix b/modules/myids.nix new file mode 100644 index 00000000..4fb26269 --- /dev/null +++ b/modules/myids.nix | |||
@@ -0,0 +1,22 @@ | |||
1 | { ... }: | ||
2 | { | ||
3 | # Check that there is no clash with nixos/modules/misc/ids.nix | ||
4 | config = { | ||
5 | ids.uids = { | ||
6 | peertube = 394; | ||
7 | redis = 395; | ||
8 | nullmailer = 396; | ||
9 | mediagoblin = 397; | ||
10 | diaspora = 398; | ||
11 | mastodon = 399; | ||
12 | }; | ||
13 | ids.gids = { | ||
14 | peertube = 394; | ||
15 | redis = 395; | ||
16 | nullmailer = 396; | ||
17 | mediagoblin = 397; | ||
18 | diaspora = 398; | ||
19 | mastodon = 399; | ||
20 | }; | ||
21 | }; | ||
22 | } | ||
diff --git a/modules/secrets.nix b/modules/secrets.nix new file mode 100644 index 00000000..b282e56e --- /dev/null +++ b/modules/secrets.nix | |||
@@ -0,0 +1,61 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | { | ||
3 | options.secrets = { | ||
4 | keys = lib.mkOption { | ||
5 | type = lib.types.listOf lib.types.unspecified; | ||
6 | default = []; | ||
7 | description = "Keys to upload to server"; | ||
8 | }; | ||
9 | location = lib.mkOption { | ||
10 | type = lib.types.path; | ||
11 | default = "/var/secrets"; | ||
12 | description = "Location where to put the keys"; | ||
13 | }; | ||
14 | }; | ||
15 | config = let | ||
16 | location = config.secrets.location; | ||
17 | keys = config.secrets.keys; | ||
18 | empty = pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out && touch $out/done"; | ||
19 | dumpKey = v: '' | ||
20 | mkdir -p secrets/$(dirname ${v.dest}) | ||
21 | echo -n ${lib.strings.escapeShellArg v.text} > secrets/${v.dest} | ||
22 | cat >> mods <<EOF | ||
23 | ${v.user or "root"} ${v.group or "root"} ${v.permissions or "0600"} secrets/${v.dest} | ||
24 | EOF | ||
25 | ''; | ||
26 | secrets = pkgs.runCommand "secrets.tar" {} '' | ||
27 | touch mods | ||
28 | tar --format=ustar --mtime='1970-01-01' -P --transform="s@${empty}@secrets@" -cf $out ${empty}/done | ||
29 | ${builtins.concatStringsSep "\n" (map dumpKey keys)} | ||
30 | cat mods | while read u g p k; do | ||
31 | tar --format=ustar --mtime='1970-01-01' --owner="$u" --group="$g" --mode="$p" --append -f $out "$k" | ||
32 | done | ||
33 | ''; | ||
34 | in lib.mkIf (builtins.length keys > 0) { | ||
35 | system.activationScripts.secrets = { | ||
36 | deps = [ "users" "wrappers" ]; | ||
37 | text = '' | ||
38 | install -m0750 -o root -g keys -d ${location} | ||
39 | if [ -f /run/keys/secrets.tar ]; then | ||
40 | if [ ! -f ${location}/currentSecrets ] || ! sha512sum -c --status "${location}/currentSecrets"; then | ||
41 | echo "rebuilding secrets" | ||
42 | rm -rf ${location} | ||
43 | install -m0750 -o root -g keys -d ${location} | ||
44 | ${pkgs.gnutar}/bin/tar --strip-components 1 -C ${location} -xf /run/keys/secrets.tar | ||
45 | sha512sum /run/keys/secrets.tar > ${location}/currentSecrets | ||
46 | find ${location} -type d -exec chown root:keys {} \; -exec chmod o-rx {} \; | ||
47 | fi | ||
48 | fi | ||
49 | ''; | ||
50 | }; | ||
51 | deployment.keys."secrets.tar" = { | ||
52 | permissions = "0400"; | ||
53 | # keyFile below is not evaluated at build time by nixops, so the | ||
54 | # `secrets` path doesn’t necessarily exist when uploading the | ||
55 | # keys, and nixops is unhappy. | ||
56 | user = "root${builtins.substring 10000 1 secrets}"; | ||
57 | group = "root"; | ||
58 | keyFile = "${secrets}"; | ||
59 | }; | ||
60 | }; | ||
61 | } | ||
diff --git a/modules/webapps/diaspora.nix b/modules/webapps/diaspora.nix new file mode 100644 index 00000000..65599b73 --- /dev/null +++ b/modules/webapps/diaspora.nix | |||
@@ -0,0 +1,171 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | name = "diaspora"; | ||
4 | cfg = config.services.diaspora; | ||
5 | |||
6 | uid = config.ids.uids.diaspora; | ||
7 | gid = config.ids.gids.diaspora; | ||
8 | in | ||
9 | { | ||
10 | options.services.diaspora = { | ||
11 | enable = lib.mkEnableOption "Enable Diaspora’s service"; | ||
12 | user = lib.mkOption { | ||
13 | type = lib.types.str; | ||
14 | default = name; | ||
15 | description = "User account under which Diaspora runs"; | ||
16 | }; | ||
17 | group = lib.mkOption { | ||
18 | type = lib.types.str; | ||
19 | default = name; | ||
20 | description = "Group under which Diaspora runs"; | ||
21 | }; | ||
22 | adminEmail = lib.mkOption { | ||
23 | type = lib.types.str; | ||
24 | example = "admin@example.com"; | ||
25 | description = "Admin e-mail for Diaspora"; | ||
26 | }; | ||
27 | dataDir = lib.mkOption { | ||
28 | type = lib.types.path; | ||
29 | default = "/var/lib/${name}"; | ||
30 | description = '' | ||
31 | The directory where Diaspora stores its data. | ||
32 | ''; | ||
33 | }; | ||
34 | socketsDir = lib.mkOption { | ||
35 | type = lib.types.path; | ||
36 | default = "/run/${name}"; | ||
37 | description = '' | ||
38 | The directory where Diaspora puts runtime files and sockets. | ||
39 | ''; | ||
40 | }; | ||
41 | configDir = lib.mkOption { | ||
42 | type = lib.types.path; | ||
43 | description = '' | ||
44 | The configuration path for Diaspora. | ||
45 | ''; | ||
46 | }; | ||
47 | package = lib.mkOption { | ||
48 | type = lib.types.package; | ||
49 | default = pkgs.webapps.diaspora; | ||
50 | description = '' | ||
51 | Diaspora package to use. | ||
52 | ''; | ||
53 | }; | ||
54 | # Output variables | ||
55 | systemdStateDirectory = lib.mkOption { | ||
56 | type = lib.types.str; | ||
57 | # Use ReadWritePaths= instead if varDir is outside of /var/lib | ||
58 | default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; | ||
59 | lib.strings.removePrefix "/var/lib/" cfg.dataDir; | ||
60 | description = '' | ||
61 | Adjusted Diaspora data directory for systemd | ||
62 | ''; | ||
63 | readOnly = true; | ||
64 | }; | ||
65 | systemdRuntimeDirectory = lib.mkOption { | ||
66 | type = lib.types.str; | ||
67 | # Use ReadWritePaths= instead if socketsDir is outside of /run | ||
68 | default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir; | ||
69 | lib.strings.removePrefix "/run/" cfg.socketsDir; | ||
70 | description = '' | ||
71 | Adjusted Diaspora sockets directory for systemd | ||
72 | ''; | ||
73 | readOnly = true; | ||
74 | }; | ||
75 | workdir = lib.mkOption { | ||
76 | type = lib.types.package; | ||
77 | default = cfg.package.override { | ||
78 | varDir = cfg.dataDir; | ||
79 | podmin_email = cfg.adminEmail; | ||
80 | config_dir = cfg.configDir; | ||
81 | }; | ||
82 | description = '' | ||
83 | Adjusted diaspora package with overriden values | ||
84 | ''; | ||
85 | readOnly = true; | ||
86 | }; | ||
87 | sockets = lib.mkOption { | ||
88 | type = lib.types.attrsOf lib.types.path; | ||
89 | default = { | ||
90 | rails = "${cfg.socketsDir}/diaspora.sock"; | ||
91 | eye = "${cfg.socketsDir}/eye.sock"; | ||
92 | }; | ||
93 | readOnly = true; | ||
94 | description = '' | ||
95 | Diaspora sockets | ||
96 | ''; | ||
97 | }; | ||
98 | pids = lib.mkOption { | ||
99 | type = lib.types.attrsOf lib.types.path; | ||
100 | default = { | ||
101 | eye = "${cfg.socketsDir}/eye.pid"; | ||
102 | }; | ||
103 | readOnly = true; | ||
104 | description = '' | ||
105 | Diaspora pids | ||
106 | ''; | ||
107 | }; | ||
108 | }; | ||
109 | |||
110 | config = lib.mkIf cfg.enable { | ||
111 | users.users = lib.optionalAttrs (cfg.user == name) (lib.singleton { | ||
112 | inherit name; | ||
113 | inherit uid; | ||
114 | group = cfg.group; | ||
115 | description = "Diaspora user"; | ||
116 | home = cfg.dataDir; | ||
117 | packages = [ cfg.workdir.gems pkgs.nodejs cfg.workdir.gems.ruby ]; | ||
118 | useDefaultShell = true; | ||
119 | }); | ||
120 | users.groups = lib.optionalAttrs (cfg.group == name) (lib.singleton { | ||
121 | inherit name; | ||
122 | inherit gid; | ||
123 | }); | ||
124 | |||
125 | systemd.services.diaspora = { | ||
126 | description = "Diaspora"; | ||
127 | wantedBy = [ "multi-user.target" ]; | ||
128 | after = [ | ||
129 | "network.target" "redis.service" "postgresql.service" | ||
130 | ]; | ||
131 | wants = [ | ||
132 | "redis.service" "postgresql.service" | ||
133 | ]; | ||
134 | |||
135 | environment.RAILS_ENV = "production"; | ||
136 | environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; | ||
137 | environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; | ||
138 | environment.EYE_SOCK = cfg.sockets.eye; | ||
139 | environment.EYE_PID = cfg.pids.eye; | ||
140 | |||
141 | path = [ cfg.workdir.gems pkgs.nodejs cfg.workdir.gems.ruby pkgs.curl pkgs.which pkgs.gawk ]; | ||
142 | |||
143 | preStart = '' | ||
144 | install -m 0755 -d ${cfg.dataDir}/uploads ${cfg.dataDir}/tmp ${cfg.dataDir}/log | ||
145 | install -m 0700 -d ${cfg.dataDir}/tmp/pids | ||
146 | if [ ! -f ${cfg.dataDir}/schedule.yml ]; then | ||
147 | echo "{}" > ${cfg.dataDir}/schedule.yml | ||
148 | fi | ||
149 | ./bin/bundle exec rails db:migrate | ||
150 | ''; | ||
151 | |||
152 | script = '' | ||
153 | exec ${cfg.workdir}/script/server | ||
154 | ''; | ||
155 | |||
156 | serviceConfig = { | ||
157 | User = cfg.user; | ||
158 | PrivateTmp = true; | ||
159 | Restart = "always"; | ||
160 | Type = "simple"; | ||
161 | WorkingDirectory = cfg.workdir; | ||
162 | StateDirectory = cfg.systemdStateDirectory; | ||
163 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
164 | StandardInput = "null"; | ||
165 | KillMode = "control-group"; | ||
166 | }; | ||
167 | |||
168 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
169 | }; | ||
170 | }; | ||
171 | } | ||
diff --git a/modules/webapps/etherpad-lite.nix b/modules/webapps/etherpad-lite.nix new file mode 100644 index 00000000..7f0e2ed4 --- /dev/null +++ b/modules/webapps/etherpad-lite.nix | |||
@@ -0,0 +1,158 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | name = "etherpad-lite"; | ||
4 | cfg = config.services.etherpad-lite; | ||
5 | |||
6 | uid = config.ids.uids.etherpad-lite; | ||
7 | gid = config.ids.gids.etherpad-lite; | ||
8 | in | ||
9 | { | ||
10 | options.services.etherpad-lite = { | ||
11 | enable = lib.mkEnableOption "Enable Etherpad lite’s service"; | ||
12 | user = lib.mkOption { | ||
13 | type = lib.types.str; | ||
14 | default = name; | ||
15 | description = "User account under which Etherpad lite runs"; | ||
16 | }; | ||
17 | group = lib.mkOption { | ||
18 | type = lib.types.str; | ||
19 | default = name; | ||
20 | description = "Group under which Etherpad lite runs"; | ||
21 | }; | ||
22 | dataDir = lib.mkOption { | ||
23 | type = lib.types.path; | ||
24 | default = "/var/lib/${name}"; | ||
25 | description = '' | ||
26 | The directory where Etherpad lite stores its data. | ||
27 | ''; | ||
28 | }; | ||
29 | socketsDir = lib.mkOption { | ||
30 | type = lib.types.path; | ||
31 | default = "/run/${name}"; | ||
32 | description = '' | ||
33 | The directory where Etherpad lite stores its sockets. | ||
34 | ''; | ||
35 | }; | ||
36 | configFile = lib.mkOption { | ||
37 | type = lib.types.path; | ||
38 | description = '' | ||
39 | The config file path for Etherpad lite. | ||
40 | ''; | ||
41 | }; | ||
42 | sessionKeyFile = lib.mkOption { | ||
43 | type = lib.types.path; | ||
44 | description = '' | ||
45 | The Session key file path for Etherpad lite. | ||
46 | ''; | ||
47 | }; | ||
48 | apiKeyFile = lib.mkOption { | ||
49 | type = lib.types.path; | ||
50 | description = '' | ||
51 | The API key file path for Etherpad lite. | ||
52 | ''; | ||
53 | }; | ||
54 | package = lib.mkOption { | ||
55 | type = lib.types.package; | ||
56 | default = pkgs.webapps.etherpad-lite; | ||
57 | description = '' | ||
58 | Etherpad lite package to use. | ||
59 | ''; | ||
60 | }; | ||
61 | modules = lib.mkOption { | ||
62 | type = lib.types.listOf lib.types.package; | ||
63 | default = []; | ||
64 | description = '' | ||
65 | Etherpad lite modules to use. | ||
66 | ''; | ||
67 | }; | ||
68 | # Output variables | ||
69 | workdir = lib.mkOption { | ||
70 | type = lib.types.package; | ||
71 | default = cfg.package.withModules cfg.modules; | ||
72 | description = '' | ||
73 | Adjusted Etherpad lite package with plugins | ||
74 | ''; | ||
75 | readOnly = true; | ||
76 | }; | ||
77 | systemdStateDirectory = lib.mkOption { | ||
78 | type = lib.types.str; | ||
79 | # Use ReadWritePaths= instead if varDir is outside of /var/lib | ||
80 | default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; | ||
81 | lib.strings.removePrefix "/var/lib/" cfg.dataDir; | ||
82 | description = '' | ||
83 | Adjusted Etherpad lite data directory for systemd | ||
84 | ''; | ||
85 | readOnly = true; | ||
86 | }; | ||
87 | systemdRuntimeDirectory = lib.mkOption { | ||
88 | type = lib.types.str; | ||
89 | # Use ReadWritePaths= instead if socketsDir is outside of /run | ||
90 | default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir; | ||
91 | lib.strings.removePrefix "/run/" cfg.socketsDir; | ||
92 | description = '' | ||
93 | Adjusted Etherpad lite sockets directory for systemd | ||
94 | ''; | ||
95 | readOnly = true; | ||
96 | }; | ||
97 | sockets = lib.mkOption { | ||
98 | type = lib.types.attrsOf lib.types.path; | ||
99 | default = { | ||
100 | node = "${cfg.socketsDir}/etherpad-lite.sock"; | ||
101 | }; | ||
102 | readOnly = true; | ||
103 | description = '' | ||
104 | Etherpad lite sockets | ||
105 | ''; | ||
106 | }; | ||
107 | }; | ||
108 | |||
109 | config = lib.mkIf cfg.enable { | ||
110 | systemd.services.etherpad-lite = { | ||
111 | description = "Etherpad-lite"; | ||
112 | wantedBy = [ "multi-user.target" ]; | ||
113 | after = [ "network.target" "postgresql.service" ]; | ||
114 | wants = [ "postgresql.service" ]; | ||
115 | |||
116 | environment.NODE_ENV = "production"; | ||
117 | environment.HOME = cfg.workdir; | ||
118 | |||
119 | path = [ pkgs.nodejs ]; | ||
120 | |||
121 | script = '' | ||
122 | exec ${pkgs.nodejs}/bin/node ${cfg.workdir}/src/node/server.js \ | ||
123 | --sessionkey ${cfg.sessionKeyFile} \ | ||
124 | --apikey ${cfg.apiKeyFile} \ | ||
125 | --settings ${cfg.configFile} | ||
126 | ''; | ||
127 | |||
128 | postStart = '' | ||
129 | while [ ! -S ${cfg.sockets.node} ]; do | ||
130 | sleep 0.5 | ||
131 | done | ||
132 | chmod a+w ${cfg.sockets.node} | ||
133 | ''; | ||
134 | serviceConfig = { | ||
135 | DynamicUser = true; | ||
136 | User = cfg.user; | ||
137 | Group = cfg.group; | ||
138 | WorkingDirectory = cfg.workdir; | ||
139 | PrivateTmp = true; | ||
140 | NoNewPrivileges = true; | ||
141 | PrivateDevices = true; | ||
142 | ProtectHome = true; | ||
143 | ProtectControlGroups = true; | ||
144 | ProtectKernelModules = true; | ||
145 | Restart = "always"; | ||
146 | Type = "simple"; | ||
147 | TimeoutSec = 60; | ||
148 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
149 | StateDirectory= cfg.systemdStateDirectory; | ||
150 | ExecStartPre = [ | ||
151 | "+${pkgs.coreutils}/bin/install -d -m 0755 -o ${cfg.user} -g ${cfg.group} ${cfg.dataDir}/ep_initialized" | ||
152 | "+${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${cfg.dataDir} ${cfg.configFile} ${cfg.sessionKeyFile} ${cfg.apiKeyFile}" | ||
153 | ]; | ||
154 | }; | ||
155 | }; | ||
156 | |||
157 | }; | ||
158 | } | ||
diff --git a/modules/webapps/mastodon.nix b/modules/webapps/mastodon.nix new file mode 100644 index 00000000..6255de91 --- /dev/null +++ b/modules/webapps/mastodon.nix | |||
@@ -0,0 +1,223 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | name = "mastodon"; | ||
4 | cfg = config.services.mastodon; | ||
5 | |||
6 | uid = config.ids.uids.mastodon; | ||
7 | gid = config.ids.gids.mastodon; | ||
8 | in | ||
9 | { | ||
10 | options.services.mastodon = { | ||
11 | enable = lib.mkEnableOption "Enable Mastodon’s service"; | ||
12 | user = lib.mkOption { | ||
13 | type = lib.types.str; | ||
14 | default = name; | ||
15 | description = "User account under which Mastodon runs"; | ||
16 | }; | ||
17 | group = lib.mkOption { | ||
18 | type = lib.types.str; | ||
19 | default = name; | ||
20 | description = "Group under which Mastodon runs"; | ||
21 | }; | ||
22 | dataDir = lib.mkOption { | ||
23 | type = lib.types.path; | ||
24 | default = "/var/lib/${name}"; | ||
25 | description = '' | ||
26 | The directory where Mastodon stores its data. | ||
27 | ''; | ||
28 | }; | ||
29 | socketsPrefix = lib.mkOption { | ||
30 | type = lib.types.string; | ||
31 | default = "live"; | ||
32 | description = '' | ||
33 | The prefix to use for Mastodon sockets. | ||
34 | ''; | ||
35 | }; | ||
36 | socketsDir = lib.mkOption { | ||
37 | type = lib.types.path; | ||
38 | default = "/run/${name}"; | ||
39 | description = '' | ||
40 | The directory where Mastodon puts runtime files and sockets. | ||
41 | ''; | ||
42 | }; | ||
43 | configFile = lib.mkOption { | ||
44 | type = lib.types.path; | ||
45 | description = '' | ||
46 | The configuration file path for Mastodon. | ||
47 | ''; | ||
48 | }; | ||
49 | package = lib.mkOption { | ||
50 | type = lib.types.package; | ||
51 | default = pkgs.webapps.mastodon; | ||
52 | description = '' | ||
53 | Mastodon package to use. | ||
54 | ''; | ||
55 | }; | ||
56 | # Output variables | ||
57 | workdir = lib.mkOption { | ||
58 | type = lib.types.package; | ||
59 | default = cfg.package.override { varDir = cfg.dataDir; }; | ||
60 | description = '' | ||
61 | Adjusted mastodon package with overriden varDir | ||
62 | ''; | ||
63 | readOnly = true; | ||
64 | }; | ||
65 | systemdStateDirectory = lib.mkOption { | ||
66 | type = lib.types.str; | ||
67 | # Use ReadWritePaths= instead if varDir is outside of /var/lib | ||
68 | default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; | ||
69 | lib.strings.removePrefix "/var/lib/" cfg.dataDir; | ||
70 | description = '' | ||
71 | Adjusted Mastodon data directory for systemd | ||
72 | ''; | ||
73 | readOnly = true; | ||
74 | }; | ||
75 | systemdRuntimeDirectory = lib.mkOption { | ||
76 | type = lib.types.str; | ||
77 | # Use ReadWritePaths= instead if socketsDir is outside of /run | ||
78 | default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir; | ||
79 | lib.strings.removePrefix "/run/" cfg.socketsDir; | ||
80 | description = '' | ||
81 | Adjusted Mastodon sockets directory for systemd | ||
82 | ''; | ||
83 | readOnly = true; | ||
84 | }; | ||
85 | sockets = lib.mkOption { | ||
86 | type = lib.types.attrsOf lib.types.path; | ||
87 | default = { | ||
88 | node = "${cfg.socketsDir}/${cfg.socketsPrefix}_node.sock"; | ||
89 | rails = "${cfg.socketsDir}/${cfg.socketsPrefix}_puma.sock"; | ||
90 | }; | ||
91 | readOnly = true; | ||
92 | description = '' | ||
93 | Mastodon sockets | ||
94 | ''; | ||
95 | }; | ||
96 | }; | ||
97 | |||
98 | config = lib.mkIf cfg.enable { | ||
99 | users.users = lib.optionalAttrs (cfg.user == name) (lib.singleton { | ||
100 | inherit name; | ||
101 | inherit uid; | ||
102 | group = cfg.group; | ||
103 | description = "Mastodon user"; | ||
104 | home = cfg.dataDir; | ||
105 | useDefaultShell = true; | ||
106 | }); | ||
107 | users.groups = lib.optionalAttrs (cfg.group == name) (lib.singleton { | ||
108 | inherit name; | ||
109 | inherit gid; | ||
110 | }); | ||
111 | |||
112 | systemd.services.mastodon-streaming = { | ||
113 | description = "Mastodon Streaming"; | ||
114 | wantedBy = [ "multi-user.target" ]; | ||
115 | after = [ "network.target" "mastodon-web.service" ]; | ||
116 | |||
117 | environment.NODE_ENV = "production"; | ||
118 | environment.SOCKET = cfg.sockets.node; | ||
119 | |||
120 | path = [ pkgs.nodejs pkgs.bashInteractive ]; | ||
121 | |||
122 | script = '' | ||
123 | exec npm run start | ||
124 | ''; | ||
125 | |||
126 | postStart = '' | ||
127 | while [ ! -S $SOCKET ]; do | ||
128 | sleep 0.5 | ||
129 | done | ||
130 | chmod a+w $SOCKET | ||
131 | ''; | ||
132 | |||
133 | postStop = '' | ||
134 | rm $SOCKET | ||
135 | ''; | ||
136 | |||
137 | serviceConfig = { | ||
138 | User = cfg.user; | ||
139 | EnvironmentFile = cfg.configFile; | ||
140 | PrivateTmp = true; | ||
141 | Restart = "always"; | ||
142 | TimeoutSec = 15; | ||
143 | Type = "simple"; | ||
144 | WorkingDirectory = cfg.workdir; | ||
145 | StateDirectory = cfg.systemdStateDirectory; | ||
146 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
147 | RuntimeDirectoryPreserve = "yes"; | ||
148 | }; | ||
149 | |||
150 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
151 | }; | ||
152 | |||
153 | systemd.services.mastodon-web = { | ||
154 | description = "Mastodon Web app"; | ||
155 | wantedBy = [ "multi-user.target" ]; | ||
156 | after = [ "network.target" ]; | ||
157 | |||
158 | environment.RAILS_ENV = "production"; | ||
159 | environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; | ||
160 | environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; | ||
161 | environment.SOCKET = cfg.sockets.rails; | ||
162 | |||
163 | path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file ]; | ||
164 | |||
165 | preStart = '' | ||
166 | install -m 0755 -d ${cfg.dataDir}/tmp/cache | ||
167 | ./bin/bundle exec rails db:migrate | ||
168 | ''; | ||
169 | |||
170 | script = '' | ||
171 | exec ./bin/bundle exec puma -C config/puma.rb | ||
172 | ''; | ||
173 | |||
174 | serviceConfig = { | ||
175 | User = cfg.user; | ||
176 | EnvironmentFile = cfg.configFile; | ||
177 | PrivateTmp = true; | ||
178 | Restart = "always"; | ||
179 | TimeoutSec = 60; | ||
180 | Type = "simple"; | ||
181 | WorkingDirectory = cfg.workdir; | ||
182 | StateDirectory = cfg.systemdStateDirectory; | ||
183 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
184 | RuntimeDirectoryPreserve = "yes"; | ||
185 | }; | ||
186 | |||
187 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
188 | }; | ||
189 | |||
190 | systemd.services.mastodon-sidekiq = { | ||
191 | description = "Mastodon Sidekiq"; | ||
192 | wantedBy = [ "multi-user.target" ]; | ||
193 | after = [ "network.target" "mastodon-web.service" ]; | ||
194 | |||
195 | environment.RAILS_ENV="production"; | ||
196 | environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; | ||
197 | environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; | ||
198 | environment.DB_POOL="5"; | ||
199 | |||
200 | path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.imagemagick pkgs.ffmpeg pkgs.file ]; | ||
201 | |||
202 | script = '' | ||
203 | exec ./bin/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push | ||
204 | ''; | ||
205 | |||
206 | serviceConfig = { | ||
207 | User = cfg.user; | ||
208 | EnvironmentFile = cfg.configFile; | ||
209 | PrivateTmp = true; | ||
210 | Restart = "always"; | ||
211 | TimeoutSec = 15; | ||
212 | Type = "simple"; | ||
213 | WorkingDirectory = cfg.workdir; | ||
214 | StateDirectory = cfg.systemdStateDirectory; | ||
215 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
216 | RuntimeDirectoryPreserve = "yes"; | ||
217 | }; | ||
218 | |||
219 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
220 | }; | ||
221 | |||
222 | }; | ||
223 | } | ||
diff --git a/modules/webapps/mediagoblin.nix b/modules/webapps/mediagoblin.nix new file mode 100644 index 00000000..78bbef6f --- /dev/null +++ b/modules/webapps/mediagoblin.nix | |||
@@ -0,0 +1,237 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | name = "mediagoblin"; | ||
4 | cfg = config.services.mediagoblin; | ||
5 | |||
6 | uid = config.ids.uids.mediagoblin; | ||
7 | gid = config.ids.gids.mediagoblin; | ||
8 | |||
9 | paste_local = pkgs.writeText "paste_local.ini" '' | ||
10 | [DEFAULT] | ||
11 | debug = false | ||
12 | |||
13 | [pipeline:main] | ||
14 | pipeline = mediagoblin | ||
15 | |||
16 | [app:mediagoblin] | ||
17 | use = egg:mediagoblin#app | ||
18 | config = ${cfg.configFile} ${cfg.workdir}/mediagoblin.ini | ||
19 | /mgoblin_static = ${cfg.workdir}/mediagoblin/static | ||
20 | |||
21 | [loggers] | ||
22 | keys = root | ||
23 | |||
24 | [handlers] | ||
25 | keys = console | ||
26 | |||
27 | [formatters] | ||
28 | keys = generic | ||
29 | |||
30 | [logger_root] | ||
31 | level = INFO | ||
32 | handlers = console | ||
33 | |||
34 | [handler_console] | ||
35 | class = StreamHandler | ||
36 | args = (sys.stderr,) | ||
37 | level = NOTSET | ||
38 | formatter = generic | ||
39 | |||
40 | [formatter_generic] | ||
41 | format = %(levelname)-7.7s [%(name)s] %(message)s | ||
42 | |||
43 | [filter:errors] | ||
44 | use = egg:mediagoblin#errors | ||
45 | debug = false | ||
46 | |||
47 | [server:main] | ||
48 | use = egg:waitress#main | ||
49 | unix_socket = ${cfg.sockets.paster} | ||
50 | unix_socket_perms = 777 | ||
51 | url_scheme = https | ||
52 | ''; | ||
53 | in | ||
54 | { | ||
55 | options.services.mediagoblin = { | ||
56 | enable = lib.mkEnableOption "Enable Mediagoblin’s service"; | ||
57 | user = lib.mkOption { | ||
58 | type = lib.types.str; | ||
59 | default = name; | ||
60 | description = "User account under which Mediagoblin runs"; | ||
61 | }; | ||
62 | group = lib.mkOption { | ||
63 | type = lib.types.str; | ||
64 | default = name; | ||
65 | description = "Group under which Mediagoblin runs"; | ||
66 | }; | ||
67 | dataDir = lib.mkOption { | ||
68 | type = lib.types.path; | ||
69 | default = "/var/lib/${name}"; | ||
70 | description = '' | ||
71 | The directory where Mediagoblin stores its data. | ||
72 | ''; | ||
73 | }; | ||
74 | socketsDir = lib.mkOption { | ||
75 | type = lib.types.path; | ||
76 | default = "/run/${name}"; | ||
77 | description = '' | ||
78 | The directory where Mediagoblin puts runtime files and sockets. | ||
79 | ''; | ||
80 | }; | ||
81 | configFile = lib.mkOption { | ||
82 | type = lib.types.path; | ||
83 | description = '' | ||
84 | The configuration file path for Mediagoblin. | ||
85 | ''; | ||
86 | }; | ||
87 | package = lib.mkOption { | ||
88 | type = lib.types.package; | ||
89 | default = pkgs.webapps.mediagoblin; | ||
90 | description = '' | ||
91 | Mediagoblin package to use. | ||
92 | ''; | ||
93 | }; | ||
94 | plugins = lib.mkOption { | ||
95 | type = lib.types.listOf lib.types.package; | ||
96 | default = []; | ||
97 | description = '' | ||
98 | Mediagoblin plugins to use. | ||
99 | ''; | ||
100 | }; | ||
101 | # Output variables | ||
102 | workdir = lib.mkOption { | ||
103 | type = lib.types.package; | ||
104 | default = cfg.package.withPlugins cfg.plugins; | ||
105 | description = '' | ||
106 | Adjusted Mediagoblin package with plugins | ||
107 | ''; | ||
108 | readOnly = true; | ||
109 | }; | ||
110 | systemdStateDirectory = lib.mkOption { | ||
111 | type = lib.types.str; | ||
112 | # Use ReadWritePaths= instead if varDir is outside of /var/lib | ||
113 | default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; | ||
114 | lib.strings.removePrefix "/var/lib/" cfg.dataDir; | ||
115 | description = '' | ||
116 | Adjusted Mediagoblin data directory for systemd | ||
117 | ''; | ||
118 | readOnly = true; | ||
119 | }; | ||
120 | systemdRuntimeDirectory = lib.mkOption { | ||
121 | type = lib.types.str; | ||
122 | # Use ReadWritePaths= instead if socketsDir is outside of /run | ||
123 | default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir; | ||
124 | lib.strings.removePrefix "/run/" cfg.socketsDir; | ||
125 | description = '' | ||
126 | Adjusted Mediagoblin sockets directory for systemd | ||
127 | ''; | ||
128 | readOnly = true; | ||
129 | }; | ||
130 | sockets = lib.mkOption { | ||
131 | type = lib.types.attrsOf lib.types.path; | ||
132 | default = { | ||
133 | paster = "${cfg.socketsDir}/mediagoblin.sock"; | ||
134 | }; | ||
135 | readOnly = true; | ||
136 | description = '' | ||
137 | Mediagoblin sockets | ||
138 | ''; | ||
139 | }; | ||
140 | pids = lib.mkOption { | ||
141 | type = lib.types.attrsOf lib.types.path; | ||
142 | default = { | ||
143 | paster = "${cfg.socketsDir}/mediagoblin.pid"; | ||
144 | celery = "${cfg.socketsDir}/mediagoblin-celeryd.pid"; | ||
145 | }; | ||
146 | readOnly = true; | ||
147 | description = '' | ||
148 | Mediagoblin pid files | ||
149 | ''; | ||
150 | }; | ||
151 | }; | ||
152 | |||
153 | config = lib.mkIf cfg.enable { | ||
154 | users.users = lib.optionalAttrs (cfg.user == name) (lib.singleton { | ||
155 | inherit name; | ||
156 | inherit uid; | ||
157 | group = cfg.group; | ||
158 | description = "Mediagoblin user"; | ||
159 | home = cfg.dataDir; | ||
160 | useDefaultShell = true; | ||
161 | }); | ||
162 | users.groups = lib.optionalAttrs (cfg.group == name) (lib.singleton { | ||
163 | inherit name; | ||
164 | inherit gid; | ||
165 | }); | ||
166 | |||
167 | systemd.services.mediagoblin-web = { | ||
168 | description = "Mediagoblin service"; | ||
169 | wantedBy = [ "multi-user.target" ]; | ||
170 | after = [ "network.target" ]; | ||
171 | wants = [ "postgresql.service" "redis.service" ]; | ||
172 | |||
173 | environment.SCRIPT_NAME = "/mediagoblin/"; | ||
174 | |||
175 | script = '' | ||
176 | exec ./bin/paster serve \ | ||
177 | ${paste_local} \ | ||
178 | --pid-file=${cfg.pids.paster} | ||
179 | ''; | ||
180 | preStop = '' | ||
181 | exec ./bin/paster serve \ | ||
182 | --pid-file=${cfg.pids.paster} \ | ||
183 | ${paste_local} stop | ||
184 | ''; | ||
185 | preStart = '' | ||
186 | if [ -d ${cfg.dataDir}/plugin_static/ ]; then | ||
187 | rm ${cfg.dataDir}/plugin_static/coreplugin_basic_auth | ||
188 | ln -sf ${cfg.workdir}/mediagoblin/plugins/basic_auth/static ${cfg.dataDir}/plugin_static/coreplugin_basic_auth | ||
189 | fi | ||
190 | ./bin/gmg -cf ${cfg.configFile} dbupdate | ||
191 | ''; | ||
192 | |||
193 | serviceConfig = { | ||
194 | User = cfg.user; | ||
195 | PrivateTmp = true; | ||
196 | Restart = "always"; | ||
197 | TimeoutSec = 15; | ||
198 | Type = "simple"; | ||
199 | WorkingDirectory = cfg.workdir; | ||
200 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
201 | StateDirectory= cfg.systemdStateDirectory; | ||
202 | PIDFile = cfg.pids.paster; | ||
203 | }; | ||
204 | |||
205 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
206 | }; | ||
207 | |||
208 | systemd.services.mediagoblin-celeryd = { | ||
209 | description = "Mediagoblin service"; | ||
210 | wantedBy = [ "multi-user.target" ]; | ||
211 | after = [ "network.target" "mediagoblin-web.service" ]; | ||
212 | |||
213 | environment.MEDIAGOBLIN_CONFIG = cfg.configFile; | ||
214 | environment.CELERY_CONFIG_MODULE = "mediagoblin.init.celery.from_celery"; | ||
215 | |||
216 | script = '' | ||
217 | exec ./bin/celery worker \ | ||
218 | --logfile=${cfg.dataDir}/celery.log \ | ||
219 | --loglevel=INFO | ||
220 | ''; | ||
221 | |||
222 | serviceConfig = { | ||
223 | User = cfg.user; | ||
224 | PrivateTmp = true; | ||
225 | Restart = "always"; | ||
226 | TimeoutSec = 60; | ||
227 | Type = "simple"; | ||
228 | WorkingDirectory = cfg.workdir; | ||
229 | RuntimeDirectory = cfg.systemdRuntimeDirectory; | ||
230 | StateDirectory= cfg.systemdStateDirectory; | ||
231 | PIDFile = cfg.pids.celery; | ||
232 | }; | ||
233 | |||
234 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
235 | }; | ||
236 | }; | ||
237 | } | ||
diff --git a/modules/webapps/peertube.nix b/modules/webapps/peertube.nix new file mode 100644 index 00000000..89dcc67a --- /dev/null +++ b/modules/webapps/peertube.nix | |||
@@ -0,0 +1,105 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | name = "peertube"; | ||
4 | cfg = config.services.peertube; | ||
5 | |||
6 | uid = config.ids.uids.peertube; | ||
7 | gid = config.ids.gids.peertube; | ||
8 | in | ||
9 | { | ||
10 | options.services.peertube = { | ||
11 | enable = lib.mkEnableOption "Enable Peertube’s service"; | ||
12 | user = lib.mkOption { | ||
13 | type = lib.types.str; | ||
14 | default = name; | ||
15 | description = "User account under which Peertube runs"; | ||
16 | }; | ||
17 | group = lib.mkOption { | ||
18 | type = lib.types.str; | ||
19 | default = name; | ||
20 | description = "Group under which Peertube runs"; | ||
21 | }; | ||
22 | dataDir = lib.mkOption { | ||
23 | type = lib.types.path; | ||
24 | default = "/var/lib/${name}"; | ||
25 | description = '' | ||
26 | The directory where Peertube stores its data. | ||
27 | ''; | ||
28 | }; | ||
29 | configFile = lib.mkOption { | ||
30 | type = lib.types.path; | ||
31 | description = '' | ||
32 | The configuration file path for Peertube. | ||
33 | ''; | ||
34 | }; | ||
35 | package = lib.mkOption { | ||
36 | type = lib.types.package; | ||
37 | default = pkgs.webapps.peertube; | ||
38 | description = '' | ||
39 | Peertube package to use. | ||
40 | ''; | ||
41 | }; | ||
42 | # Output variables | ||
43 | systemdStateDirectory = lib.mkOption { | ||
44 | type = lib.types.str; | ||
45 | # Use ReadWritePaths= instead if varDir is outside of /var/lib | ||
46 | default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; | ||
47 | lib.strings.removePrefix "/var/lib/" cfg.dataDir; | ||
48 | description = '' | ||
49 | Adjusted Peertube data directory for systemd | ||
50 | ''; | ||
51 | readOnly = true; | ||
52 | }; | ||
53 | }; | ||
54 | |||
55 | config = lib.mkIf cfg.enable { | ||
56 | users.users = lib.optionalAttrs (cfg.user == name) (lib.singleton { | ||
57 | inherit name; | ||
58 | inherit uid; | ||
59 | group = cfg.group; | ||
60 | description = "Peertube user"; | ||
61 | home = cfg.dataDir; | ||
62 | useDefaultShell = true; | ||
63 | }); | ||
64 | users.groups = lib.optionalAttrs (cfg.group == name) (lib.singleton { | ||
65 | inherit name; | ||
66 | inherit gid; | ||
67 | }); | ||
68 | |||
69 | systemd.services.peertube = { | ||
70 | description = "Peertube"; | ||
71 | wantedBy = [ "multi-user.target" ]; | ||
72 | after = [ "network.target" "postgresql.service" ]; | ||
73 | wants = [ "postgresql.service" ]; | ||
74 | |||
75 | environment.NODE_CONFIG_DIR = "${cfg.dataDir}/config"; | ||
76 | environment.NODE_ENV = "production"; | ||
77 | environment.HOME = cfg.package; | ||
78 | |||
79 | path = [ pkgs.nodejs pkgs.bashInteractive pkgs.ffmpeg pkgs.openssl ]; | ||
80 | |||
81 | script = '' | ||
82 | install -m 0750 -d ${cfg.dataDir}/config | ||
83 | ln -sf ${cfg.configFile} ${cfg.dataDir}/config/production.yaml | ||
84 | exec npm run start | ||
85 | ''; | ||
86 | |||
87 | serviceConfig = { | ||
88 | User = cfg.user; | ||
89 | Group = cfg.group; | ||
90 | WorkingDirectory = cfg.package; | ||
91 | StateDirectory = cfg.systemdStateDirectory; | ||
92 | StateDirectoryMode = 0750; | ||
93 | PrivateTmp = true; | ||
94 | ProtectHome = true; | ||
95 | ProtectControlGroups = true; | ||
96 | Restart = "always"; | ||
97 | Type = "simple"; | ||
98 | TimeoutSec = 60; | ||
99 | }; | ||
100 | |||
101 | unitConfig.RequiresMountsFor = cfg.dataDir; | ||
102 | }; | ||
103 | }; | ||
104 | } | ||
105 | |||
diff --git a/modules/webapps/webstats/default.nix b/modules/webapps/webstats/default.nix new file mode 100644 index 00000000..924d72de --- /dev/null +++ b/modules/webapps/webstats/default.nix | |||
@@ -0,0 +1,81 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | name = "goaccess"; | ||
4 | cfg = config.services.webstats; | ||
5 | in { | ||
6 | options.services.webstats = { | ||
7 | dataDir = lib.mkOption { | ||
8 | type = lib.types.path; | ||
9 | default = "/var/lib/${name}"; | ||
10 | description = '' | ||
11 | The directory where Goaccess stores its data. | ||
12 | ''; | ||
13 | }; | ||
14 | sites = lib.mkOption { | ||
15 | type = lib.types.listOf (lib.types.submodule { | ||
16 | options = { | ||
17 | conf = lib.mkOption { | ||
18 | type = lib.types.nullOr lib.types.path; | ||
19 | default = null; | ||
20 | description = '' | ||
21 | use custom goaccess configuration file instead of the | ||
22 | default one. | ||
23 | ''; | ||
24 | }; | ||
25 | name = lib.mkOption { | ||
26 | type = lib.types.string; | ||
27 | description = '' | ||
28 | Domain name. Corresponds to the Apache file name and the | ||
29 | folder name in which the state will be saved. | ||
30 | ''; | ||
31 | }; | ||
32 | }; | ||
33 | }); | ||
34 | default = []; | ||
35 | description = "Sites to generate stats"; | ||
36 | }; | ||
37 | }; | ||
38 | |||
39 | config = lib.mkIf (builtins.length cfg.sites > 0) { | ||
40 | users.users.root.packages = [ | ||
41 | pkgs.goaccess | ||
42 | ]; | ||
43 | |||
44 | services.cron = { | ||
45 | enable = true; | ||
46 | systemCronJobs = let | ||
47 | stats = domain: conf: let | ||
48 | config = if builtins.isNull conf | ||
49 | then pkgs.runCommand "goaccess.conf" { | ||
50 | dbPath = "${cfg.dataDir}/${domain}"; | ||
51 | } "substituteAll ${./goaccess.conf} $out" | ||
52 | else conf; | ||
53 | d = pkgs.writeScriptBin "stats-${domain}" '' | ||
54 | #!${pkgs.stdenv.shell} | ||
55 | set -e | ||
56 | shopt -s nullglob | ||
57 | date_regex=$(LC_ALL=C date -d yesterday +'%d\/%b\/%Y') | ||
58 | TMPFILE=$(mktemp) | ||
59 | trap "rm -f $TMPFILE" EXIT | ||
60 | |||
61 | mkdir -p ${cfg.dataDir}/${domain} | ||
62 | cat /var/log/httpd/access-${domain}.log | sed -n "/\\[$date_regex/ p" > $TMPFILE | ||
63 | for i in /var/log/httpd/access-${domain}*.gz; do | ||
64 | zcat "$i" | sed -n "/\\[$date_regex/ p" >> $TMPFILE | ||
65 | done | ||
66 | ${pkgs.goaccess}/bin/goaccess $TMPFILE --no-progress -o ${cfg.dataDir}/${domain}/index.html -p ${config} | ||
67 | ''; | ||
68 | in "${d}/bin/stats-${domain}"; | ||
69 | allStats = sites: pkgs.writeScript "stats" '' | ||
70 | #!${pkgs.stdenv.shell} | ||
71 | |||
72 | mkdir -p ${cfg.dataDir} | ||
73 | ${builtins.concatStringsSep "\n" (map (v: stats v.name v.conf) sites)} | ||
74 | ''; | ||
75 | in | ||
76 | [ | ||
77 | "5 0 * * * root ${allStats cfg.sites}" | ||
78 | ]; | ||
79 | }; | ||
80 | }; | ||
81 | } | ||
diff --git a/modules/webapps/webstats/goaccess.conf b/modules/webapps/webstats/goaccess.conf new file mode 100644 index 00000000..49189883 --- /dev/null +++ b/modules/webapps/webstats/goaccess.conf | |||
@@ -0,0 +1,99 @@ | |||
1 | time-format %H:%M:%S | ||
2 | date-format %d/%b/%Y | ||
3 | |||
4 | #sur immae.eu | ||
5 | #log-format %v %h %^[%d:%t %^] "%r" %s %b "%R" "%u" $^ | ||
6 | |||
7 | log-format VCOMBINED | ||
8 | #= %v:%^ %h %^[%d:%t %^] "%r" %s %b "%R" "%u" | ||
9 | |||
10 | html-prefs {"theme":"bright","layout":"vertical"} | ||
11 | |||
12 | exclude-ip 188.165.209.148 | ||
13 | exclude-ip 178.33.252.96 | ||
14 | exclude-ip 2001:41d0:2:9c94::1 | ||
15 | exclude-ip 2001:41d0:2:9c94:: | ||
16 | exclude-ip 176.9.151.89 | ||
17 | exclude-ip 2a01:4f8:160:3445:: | ||
18 | exclude-ip 82.255.56.72 | ||
19 | |||
20 | no-query-string true | ||
21 | |||
22 | keep-db-files true | ||
23 | load-from-disk true | ||
24 | db-path @dbPath@ | ||
25 | |||
26 | ignore-panel REFERRERS | ||
27 | ignore-panel KEYPHRASES | ||
28 | |||
29 | static-file .css | ||
30 | static-file .js | ||
31 | static-file .jpg | ||
32 | static-file .png | ||
33 | static-file .gif | ||
34 | static-file .ico | ||
35 | static-file .jpeg | ||
36 | static-file .pdf | ||
37 | static-file .csv | ||
38 | static-file .mpeg | ||
39 | static-file .mpg | ||
40 | static-file .swf | ||
41 | static-file .woff | ||
42 | static-file .woff2 | ||
43 | static-file .xls | ||
44 | static-file .xlsx | ||
45 | static-file .doc | ||
46 | static-file .docx | ||
47 | static-file .ppt | ||
48 | static-file .pptx | ||
49 | static-file .txt | ||
50 | static-file .zip | ||
51 | static-file .ogg | ||
52 | static-file .mp3 | ||
53 | static-file .mp4 | ||
54 | static-file .exe | ||
55 | static-file .iso | ||
56 | static-file .gz | ||
57 | static-file .rar | ||
58 | static-file .svg | ||
59 | static-file .bmp | ||
60 | static-file .tar | ||
61 | static-file .tgz | ||
62 | static-file .tiff | ||
63 | static-file .tif | ||
64 | static-file .ttf | ||
65 | static-file .flv | ||
66 | #static-file .less | ||
67 | #static-file .ac3 | ||
68 | #static-file .avi | ||
69 | #static-file .bz2 | ||
70 | #static-file .class | ||
71 | #static-file .cue | ||
72 | #static-file .dae | ||
73 | #static-file .dat | ||
74 | #static-file .dts | ||
75 | #static-file .ejs | ||
76 | #static-file .eot | ||
77 | #static-file .eps | ||
78 | #static-file .img | ||
79 | #static-file .jar | ||
80 | #static-file .map | ||
81 | #static-file .mid | ||
82 | #static-file .midi | ||
83 | #static-file .ogv | ||
84 | #static-file .webm | ||
85 | #static-file .mkv | ||
86 | #static-file .odp | ||
87 | #static-file .ods | ||
88 | #static-file .odt | ||
89 | #static-file .otf | ||
90 | #static-file .pict | ||
91 | #static-file .pls | ||
92 | #static-file .ps | ||
93 | #static-file .qt | ||
94 | #static-file .rm | ||
95 | #static-file .svgz | ||
96 | #static-file .wav | ||
97 | #static-file .webp | ||
98 | |||
99 | |||
diff --git a/modules/websites/default.nix b/modules/websites/default.nix new file mode 100644 index 00000000..e57f505a --- /dev/null +++ b/modules/websites/default.nix | |||
@@ -0,0 +1,199 @@ | |||
1 | { lib, config, ... }: with lib; | ||
2 | let | ||
3 | cfg = config.services.websites; | ||
4 | in | ||
5 | { | ||
6 | options.services.websitesCerts = mkOption { | ||
7 | description = "Default websites configuration for certificates as accepted by acme"; | ||
8 | }; | ||
9 | options.services.websites = with types; mkOption { | ||
10 | default = {}; | ||
11 | description = "Each type of website to enable will target a distinct httpd server"; | ||
12 | type = attrsOf (submodule { | ||
13 | options = { | ||
14 | enable = mkEnableOption "Enable websites of this type"; | ||
15 | adminAddr = mkOption { | ||
16 | type = str; | ||
17 | description = "Admin e-mail address of the instance"; | ||
18 | }; | ||
19 | httpdName = mkOption { | ||
20 | type = str; | ||
21 | description = "Name of the httpd instance to assign this type to"; | ||
22 | }; | ||
23 | ips = mkOption { | ||
24 | type = listOf string; | ||
25 | default = []; | ||
26 | description = "ips to listen to"; | ||
27 | }; | ||
28 | modules = mkOption { | ||
29 | type = listOf str; | ||
30 | default = []; | ||
31 | description = "Additional modules to load in Apache"; | ||
32 | }; | ||
33 | extraConfig = mkOption { | ||
34 | type = listOf lines; | ||
35 | default = []; | ||
36 | description = "Additional configuration to append to Apache"; | ||
37 | }; | ||
38 | nosslVhost = mkOption { | ||
39 | description = "A default nossl vhost for captive portals"; | ||
40 | default = {}; | ||
41 | type = submodule { | ||
42 | options = { | ||
43 | enable = mkEnableOption "Add default no-ssl vhost for this instance"; | ||
44 | host = mkOption { | ||
45 | type = string; | ||
46 | description = "The hostname to use for this vhost"; | ||
47 | }; | ||
48 | root = mkOption { | ||
49 | type = path; | ||
50 | default = ./nosslVhost; | ||
51 | description = "The root folder to serve"; | ||
52 | }; | ||
53 | indexFile = mkOption { | ||
54 | type = string; | ||
55 | default = "index.html"; | ||
56 | description = "The index file to show."; | ||
57 | }; | ||
58 | }; | ||
59 | }; | ||
60 | }; | ||
61 | fallbackVhost = mkOption { | ||
62 | description = "The fallback vhost that will be defined as first vhost in Apache"; | ||
63 | type = submodule { | ||
64 | options = { | ||
65 | certName = mkOption { type = string; }; | ||
66 | hosts = mkOption { type = listOf string; }; | ||
67 | root = mkOption { type = nullOr path; }; | ||
68 | extraConfig = mkOption { type = listOf lines; default = []; }; | ||
69 | }; | ||
70 | }; | ||
71 | }; | ||
72 | vhostConfs = mkOption { | ||
73 | default = {}; | ||
74 | description = "List of vhosts to define for Apache"; | ||
75 | type = attrsOf (submodule { | ||
76 | options = { | ||
77 | certName = mkOption { type = string; }; | ||
78 | addToCerts = mkOption { | ||
79 | type = bool; | ||
80 | default = false; | ||
81 | description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null"; | ||
82 | }; | ||
83 | certMainHost = mkOption { | ||
84 | type = nullOr string; | ||
85 | description = "Use that host as 'main host' for acme certs"; | ||
86 | default = null; | ||
87 | }; | ||
88 | hosts = mkOption { type = listOf string; }; | ||
89 | root = mkOption { type = nullOr path; }; | ||
90 | extraConfig = mkOption { type = listOf lines; default = []; }; | ||
91 | }; | ||
92 | }); | ||
93 | }; | ||
94 | }; | ||
95 | }); | ||
96 | }; | ||
97 | |||
98 | config.services.httpd = let | ||
99 | redirectVhost = ips: { # Should go last, catchall http -> https redirect | ||
100 | listen = map (ip: { inherit ip; port = 80; }) ips; | ||
101 | hostName = "redirectSSL"; | ||
102 | serverAliases = [ "*" ]; | ||
103 | enableSSL = false; | ||
104 | logFormat = "combinedVhost"; | ||
105 | documentRoot = "${config.security.acme.directory}/acme-challenge"; | ||
106 | extraConfig = '' | ||
107 | RewriteEngine on | ||
108 | RewriteCond "%{REQUEST_URI}" "!^/\.well-known" | ||
109 | RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301] | ||
110 | # To redirect in specific "VirtualHost *:80", do | ||
111 | # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1 | ||
112 | # rather than rewrite | ||
113 | ''; | ||
114 | }; | ||
115 | nosslVhost = ips: cfg: { | ||
116 | listen = map (ip: { inherit ip; port = 80; }) ips; | ||
117 | hostName = cfg.host; | ||
118 | enableSSL = false; | ||
119 | logFormat = "combinedVhost"; | ||
120 | documentRoot = cfg.root; | ||
121 | extraConfig = '' | ||
122 | <Directory ${cfg.root}> | ||
123 | DirectoryIndex ${cfg.indexFile} | ||
124 | AllowOverride None | ||
125 | Require all granted | ||
126 | |||
127 | RewriteEngine on | ||
128 | RewriteRule ^/(.+) / [L] | ||
129 | </Directory> | ||
130 | ''; | ||
131 | }; | ||
132 | toVhost = ips: vhostConf: { | ||
133 | enableSSL = true; | ||
134 | sslServerCert = "${config.security.acme.directory}/${vhostConf.certName}/cert.pem"; | ||
135 | sslServerKey = "${config.security.acme.directory}/${vhostConf.certName}/key.pem"; | ||
136 | sslServerChain = "${config.security.acme.directory}/${vhostConf.certName}/chain.pem"; | ||
137 | logFormat = "combinedVhost"; | ||
138 | listen = map (ip: { inherit ip; port = 443; }) ips; | ||
139 | hostName = builtins.head vhostConf.hosts; | ||
140 | serverAliases = builtins.tail vhostConf.hosts or []; | ||
141 | documentRoot = vhostConf.root; | ||
142 | extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig; | ||
143 | }; | ||
144 | in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair | ||
145 | icfg.httpdName (mkIf icfg.enable { | ||
146 | enable = true; | ||
147 | listen = map (ip: { inherit ip; port = 443; }) icfg.ips; | ||
148 | stateDir = "/run/httpd_${name}"; | ||
149 | logPerVirtualHost = true; | ||
150 | multiProcessingModule = "worker"; | ||
151 | inherit (icfg) adminAddr; | ||
152 | logFormat = "combinedVhost"; | ||
153 | extraModules = lists.unique icfg.modules; | ||
154 | extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig; | ||
155 | virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ] | ||
156 | ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ] | ||
157 | ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs) | ||
158 | ++ [ (redirectVhost icfg.ips) ]; | ||
159 | }) | ||
160 | ) cfg; | ||
161 | |||
162 | config.security.acme.certs = let | ||
163 | typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg; | ||
164 | flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v: | ||
165 | attrValues v.vhostConfs | ||
166 | ) typesToManage); | ||
167 | groupedCerts = attrsets.filterAttrs | ||
168 | (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group) | ||
169 | (lists.groupBy (v: v.certName) flatVhosts); | ||
170 | groupToDomain = group: | ||
171 | let | ||
172 | nonNull = builtins.filter (v: !isNull v.certMainHost) group; | ||
173 | domains = lists.unique (map (v: v.certMainHost) nonNull); | ||
174 | in | ||
175 | if builtins.length domains == 0 | ||
176 | then null | ||
177 | else assert (builtins.length domains == 1); (elemAt domains 0); | ||
178 | extraDomains = group: | ||
179 | let | ||
180 | mainDomain = groupToDomain group; | ||
181 | in | ||
182 | lists.remove mainDomain ( | ||
183 | lists.unique ( | ||
184 | lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group) | ||
185 | ) | ||
186 | ); | ||
187 | in attrsets.mapAttrs (k: g: | ||
188 | if (!isNull (groupToDomain g)) | ||
189 | then config.services.websitesCerts // { | ||
190 | domain = groupToDomain g; | ||
191 | extraDomains = builtins.listToAttrs ( | ||
192 | map (d: attrsets.nameValuePair d null) (extraDomains g)); | ||
193 | } | ||
194 | else { | ||
195 | extraDomains = builtins.listToAttrs ( | ||
196 | map (d: attrsets.nameValuePair d null) (extraDomains g)); | ||
197 | } | ||
198 | ) groupedCerts; | ||
199 | } | ||
diff --git a/modules/websites/httpd-service-builder.nix b/modules/websites/httpd-service-builder.nix new file mode 100644 index 00000000..d049202c --- /dev/null +++ b/modules/websites/httpd-service-builder.nix | |||
@@ -0,0 +1,746 @@ | |||
1 | # to help backporting this builder should stay as close as possible to | ||
2 | # nixos/modules/services/web-servers/apache-httpd/default.nix | ||
3 | { httpdName, withUsers ? true }: | ||
4 | { config, lib, pkgs, ... }: | ||
5 | |||
6 | with lib; | ||
7 | |||
8 | let | ||
9 | |||
10 | mainCfg = config.services.httpd."${httpdName}"; | ||
11 | |||
12 | httpd = mainCfg.package.out; | ||
13 | |||
14 | version24 = !versionOlder httpd.version "2.4"; | ||
15 | |||
16 | httpdConf = mainCfg.configFile; | ||
17 | |||
18 | php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ }; | ||
19 | |||
20 | phpMajorVersion = head (splitString "." php.version); | ||
21 | |||
22 | mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; }; | ||
23 | |||
24 | defaultListen = cfg: if cfg.enableSSL | ||
25 | then [{ip = "*"; port = 443;}] | ||
26 | else [{ip = "*"; port = 80;}]; | ||
27 | |||
28 | getListen = cfg: | ||
29 | let list = (lib.optional (cfg.port != 0) {ip = "*"; port = cfg.port;}) ++ cfg.listen; | ||
30 | in if list == [] | ||
31 | then defaultListen cfg | ||
32 | else list; | ||
33 | |||
34 | listenToString = l: "${l.ip}:${toString l.port}"; | ||
35 | |||
36 | extraModules = attrByPath ["extraModules"] [] mainCfg; | ||
37 | extraForeignModules = filter isAttrs extraModules; | ||
38 | extraApacheModules = filter isString extraModules; | ||
39 | |||
40 | |||
41 | makeServerInfo = cfg: { | ||
42 | # Canonical name must not include a trailing slash. | ||
43 | canonicalNames = | ||
44 | let defaultPort = (head (defaultListen cfg)).port; in | ||
45 | map (port: | ||
46 | (if cfg.enableSSL then "https" else "http") + "://" + | ||
47 | cfg.hostName + | ||
48 | (if port != defaultPort then ":${toString port}" else "") | ||
49 | ) (map (x: x.port) (getListen cfg)); | ||
50 | |||
51 | # Admin address: inherit from the main server if not specified for | ||
52 | # a virtual host. | ||
53 | adminAddr = if cfg.adminAddr != null then cfg.adminAddr else mainCfg.adminAddr; | ||
54 | |||
55 | vhostConfig = cfg; | ||
56 | serverConfig = mainCfg; | ||
57 | fullConfig = config; # machine config | ||
58 | }; | ||
59 | |||
60 | |||
61 | allHosts = [mainCfg] ++ mainCfg.virtualHosts; | ||
62 | |||
63 | |||
64 | callSubservices = serverInfo: defs: | ||
65 | let f = svc: | ||
66 | let | ||
67 | svcFunction = | ||
68 | if svc ? function then svc.function | ||
69 | # instead of using serviceType="mediawiki"; you can copy mediawiki.nix to any location outside nixpkgs, modify it at will, and use serviceExpression=./mediawiki.nix; | ||
70 | else if svc ? serviceExpression then import (toString svc.serviceExpression) | ||
71 | else import (toString "${toString ./.}/${if svc ? serviceType then svc.serviceType else svc.serviceName}.nix"); | ||
72 | config = (evalModules | ||
73 | { modules = [ { options = res.options; config = svc.config or svc; } ]; | ||
74 | check = false; | ||
75 | }).config; | ||
76 | defaults = { | ||
77 | extraConfig = ""; | ||
78 | extraModules = []; | ||
79 | extraModulesPre = []; | ||
80 | extraPath = []; | ||
81 | extraServerPath = []; | ||
82 | globalEnvVars = []; | ||
83 | robotsEntries = ""; | ||
84 | startupScript = ""; | ||
85 | enablePHP = false; | ||
86 | enablePerl = false; | ||
87 | phpOptions = ""; | ||
88 | options = {}; | ||
89 | documentRoot = null; | ||
90 | }; | ||
91 | res = defaults // svcFunction { inherit config lib pkgs serverInfo php; }; | ||
92 | in res; | ||
93 | in map f defs; | ||
94 | |||
95 | |||
96 | # !!! callSubservices is expensive | ||
97 | subservicesFor = cfg: callSubservices (makeServerInfo cfg) cfg.extraSubservices; | ||
98 | |||
99 | mainSubservices = subservicesFor mainCfg; | ||
100 | |||
101 | allSubservices = mainSubservices ++ concatMap subservicesFor mainCfg.virtualHosts; | ||
102 | |||
103 | |||
104 | enableSSL = any (vhost: vhost.enableSSL) allHosts; | ||
105 | |||
106 | |||
107 | # Names of modules from ${httpd}/modules that we want to load. | ||
108 | apacheModules = | ||
109 | [ # HTTP authentication mechanisms: basic and digest. | ||
110 | "auth_basic" "auth_digest" | ||
111 | |||
112 | # Authentication: is the user who he claims to be? | ||
113 | "authn_file" "authn_dbm" "authn_anon" | ||
114 | (if version24 then "authn_core" else "authn_alias") | ||
115 | |||
116 | # Authorization: is the user allowed access? | ||
117 | "authz_user" "authz_groupfile" "authz_host" | ||
118 | |||
119 | # Other modules. | ||
120 | "ext_filter" "include" "log_config" "env" "mime_magic" | ||
121 | "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif" | ||
122 | "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs" | ||
123 | "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling" | ||
124 | "userdir" "alias" "rewrite" "proxy" "proxy_http" | ||
125 | ] | ||
126 | ++ optionals version24 [ | ||
127 | "mpm_${mainCfg.multiProcessingModule}" | ||
128 | "authz_core" | ||
129 | "unixd" | ||
130 | "cache" "cache_disk" | ||
131 | "slotmem_shm" | ||
132 | "socache_shmcb" | ||
133 | # For compatibility with old configurations, the new module mod_access_compat is provided. | ||
134 | "access_compat" | ||
135 | ] | ||
136 | ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ]) | ||
137 | ++ optional enableSSL "ssl" | ||
138 | ++ extraApacheModules; | ||
139 | |||
140 | |||
141 | allDenied = if version24 then '' | ||
142 | Require all denied | ||
143 | '' else '' | ||
144 | Order deny,allow | ||
145 | Deny from all | ||
146 | ''; | ||
147 | |||
148 | allGranted = if version24 then '' | ||
149 | Require all granted | ||
150 | '' else '' | ||
151 | Order allow,deny | ||
152 | Allow from all | ||
153 | ''; | ||
154 | |||
155 | |||
156 | loggingConf = (if mainCfg.logFormat != "none" then '' | ||
157 | ErrorLog ${mainCfg.logDir}/error.log | ||
158 | |||
159 | LogLevel notice | ||
160 | |||
161 | LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined | ||
162 | LogFormat "%h %l %u %t \"%r\" %>s %b" common | ||
163 | LogFormat "%{Referer}i -> %U" referer | ||
164 | LogFormat "%{User-agent}i" agent | ||
165 | |||
166 | CustomLog ${mainCfg.logDir}/access.log ${mainCfg.logFormat} | ||
167 | '' else '' | ||
168 | ErrorLog /dev/null | ||
169 | ''); | ||
170 | |||
171 | |||
172 | browserHacks = '' | ||
173 | BrowserMatch "Mozilla/2" nokeepalive | ||
174 | BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 | ||
175 | BrowserMatch "RealPlayer 4\.0" force-response-1.0 | ||
176 | BrowserMatch "Java/1\.0" force-response-1.0 | ||
177 | BrowserMatch "JDK/1\.0" force-response-1.0 | ||
178 | BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully | ||
179 | BrowserMatch "^WebDrive" redirect-carefully | ||
180 | BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully | ||
181 | BrowserMatch "^gnome-vfs" redirect-carefully | ||
182 | ''; | ||
183 | |||
184 | |||
185 | sslConf = '' | ||
186 | SSLSessionCache ${if version24 then "shmcb" else "shm"}:${mainCfg.stateDir}/ssl_scache(512000) | ||
187 | |||
188 | ${if version24 then "Mutex" else "SSLMutex"} posixsem | ||
189 | |||
190 | SSLRandomSeed startup builtin | ||
191 | SSLRandomSeed connect builtin | ||
192 | |||
193 | SSLProtocol ${mainCfg.sslProtocols} | ||
194 | SSLCipherSuite ${mainCfg.sslCiphers} | ||
195 | SSLHonorCipherOrder on | ||
196 | ''; | ||
197 | |||
198 | |||
199 | mimeConf = '' | ||
200 | TypesConfig ${httpd}/conf/mime.types | ||
201 | |||
202 | AddType application/x-x509-ca-cert .crt | ||
203 | AddType application/x-pkcs7-crl .crl | ||
204 | AddType application/x-httpd-php .php .phtml | ||
205 | |||
206 | <IfModule mod_mime_magic.c> | ||
207 | MIMEMagicFile ${httpd}/conf/magic | ||
208 | </IfModule> | ||
209 | ''; | ||
210 | |||
211 | |||
212 | perServerConf = isMainServer: cfg: let | ||
213 | |||
214 | serverInfo = makeServerInfo cfg; | ||
215 | |||
216 | subservices = callSubservices serverInfo cfg.extraSubservices; | ||
217 | |||
218 | maybeDocumentRoot = fold (svc: acc: | ||
219 | if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc | ||
220 | ) null ([ cfg ] ++ subservices); | ||
221 | |||
222 | documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else | ||
223 | pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"; | ||
224 | |||
225 | documentRootConf = '' | ||
226 | DocumentRoot "${documentRoot}" | ||
227 | |||
228 | <Directory "${documentRoot}"> | ||
229 | Options Indexes FollowSymLinks | ||
230 | AllowOverride None | ||
231 | ${allGranted} | ||
232 | </Directory> | ||
233 | ''; | ||
234 | |||
235 | robotsTxt = | ||
236 | concatStringsSep "\n" (filter (x: x != "") ( | ||
237 | # If this is a vhost, the include the entries for the main server as well. | ||
238 | (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices) | ||
239 | ++ [cfg.robotsEntries] | ||
240 | ++ (map (svc: svc.robotsEntries) subservices))); | ||
241 | |||
242 | in '' | ||
243 | ${concatStringsSep "\n" (map (n: "ServerName ${n}") serverInfo.canonicalNames)} | ||
244 | |||
245 | ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases} | ||
246 | |||
247 | ${if cfg.sslServerCert != null then '' | ||
248 | SSLCertificateFile ${cfg.sslServerCert} | ||
249 | SSLCertificateKeyFile ${cfg.sslServerKey} | ||
250 | ${if cfg.sslServerChain != null then '' | ||
251 | SSLCertificateChainFile ${cfg.sslServerChain} | ||
252 | '' else ""} | ||
253 | '' else ""} | ||
254 | |||
255 | ${if cfg.enableSSL then '' | ||
256 | SSLEngine on | ||
257 | '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */ | ||
258 | '' | ||
259 | SSLEngine off | ||
260 | '' else ""} | ||
261 | |||
262 | ${if isMainServer || cfg.adminAddr != null then '' | ||
263 | ServerAdmin ${cfg.adminAddr} | ||
264 | '' else ""} | ||
265 | |||
266 | ${if !isMainServer && mainCfg.logPerVirtualHost then '' | ||
267 | ErrorLog ${mainCfg.logDir}/error-${cfg.hostName}.log | ||
268 | CustomLog ${mainCfg.logDir}/access-${cfg.hostName}.log ${cfg.logFormat} | ||
269 | '' else ""} | ||
270 | |||
271 | ${optionalString (robotsTxt != "") '' | ||
272 | Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt} | ||
273 | ''} | ||
274 | |||
275 | ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""} | ||
276 | |||
277 | ${if cfg.enableUserDir then '' | ||
278 | |||
279 | UserDir public_html | ||
280 | UserDir disabled root | ||
281 | |||
282 | <Directory "/home/*/public_html"> | ||
283 | AllowOverride FileInfo AuthConfig Limit Indexes | ||
284 | Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec | ||
285 | <Limit GET POST OPTIONS> | ||
286 | ${allGranted} | ||
287 | </Limit> | ||
288 | <LimitExcept GET POST OPTIONS> | ||
289 | ${allDenied} | ||
290 | </LimitExcept> | ||
291 | </Directory> | ||
292 | |||
293 | '' else ""} | ||
294 | |||
295 | ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then '' | ||
296 | RedirectPermanent / ${cfg.globalRedirect} | ||
297 | '' else ""} | ||
298 | |||
299 | ${ | ||
300 | let makeFileConf = elem: '' | ||
301 | Alias ${elem.urlPath} ${elem.file} | ||
302 | ''; | ||
303 | in concatMapStrings makeFileConf cfg.servedFiles | ||
304 | } | ||
305 | |||
306 | ${ | ||
307 | let makeDirConf = elem: '' | ||
308 | Alias ${elem.urlPath} ${elem.dir}/ | ||
309 | <Directory ${elem.dir}> | ||
310 | Options +Indexes | ||
311 | ${allGranted} | ||
312 | AllowOverride All | ||
313 | </Directory> | ||
314 | ''; | ||
315 | in concatMapStrings makeDirConf cfg.servedDirs | ||
316 | } | ||
317 | |||
318 | ${concatMapStrings (svc: svc.extraConfig) subservices} | ||
319 | |||
320 | ${cfg.extraConfig} | ||
321 | ''; | ||
322 | |||
323 | |||
324 | confFile = pkgs.writeText "httpd.conf" '' | ||
325 | |||
326 | ServerRoot ${httpd} | ||
327 | |||
328 | ${optionalString version24 '' | ||
329 | DefaultRuntimeDir ${mainCfg.stateDir}/runtime | ||
330 | ''} | ||
331 | |||
332 | PidFile ${mainCfg.stateDir}/httpd.pid | ||
333 | |||
334 | ${optionalString (mainCfg.multiProcessingModule != "prefork") '' | ||
335 | # mod_cgid requires this. | ||
336 | ScriptSock ${mainCfg.stateDir}/cgisock | ||
337 | ''} | ||
338 | |||
339 | <IfModule prefork.c> | ||
340 | MaxClients ${toString mainCfg.maxClients} | ||
341 | MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild} | ||
342 | </IfModule> | ||
343 | |||
344 | ${let | ||
345 | listen = concatMap getListen allHosts; | ||
346 | toStr = listen: "Listen ${listenToString listen}\n"; | ||
347 | uniqueListen = uniqList {inputList = map toStr listen;}; | ||
348 | in concatStrings uniqueListen | ||
349 | } | ||
350 | |||
351 | User ${mainCfg.user} | ||
352 | Group ${mainCfg.group} | ||
353 | |||
354 | ${let | ||
355 | load = {name, path}: "LoadModule ${name}_module ${path}\n"; | ||
356 | allModules = | ||
357 | concatMap (svc: svc.extraModulesPre) allSubservices | ||
358 | ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules | ||
359 | ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } | ||
360 | ++ optional enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; } | ||
361 | ++ optional enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } | ||
362 | ++ concatMap (svc: svc.extraModules) allSubservices | ||
363 | ++ extraForeignModules; | ||
364 | in concatMapStrings load allModules | ||
365 | } | ||
366 | |||
367 | AddHandler type-map var | ||
368 | |||
369 | <Files ~ "^\.ht"> | ||
370 | ${allDenied} | ||
371 | </Files> | ||
372 | |||
373 | ${mimeConf} | ||
374 | ${loggingConf} | ||
375 | ${browserHacks} | ||
376 | |||
377 | Include ${httpd}/conf/extra/httpd-default.conf | ||
378 | Include ${httpd}/conf/extra/httpd-autoindex.conf | ||
379 | Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf | ||
380 | Include ${httpd}/conf/extra/httpd-languages.conf | ||
381 | |||
382 | TraceEnable off | ||
383 | |||
384 | ${if enableSSL then sslConf else ""} | ||
385 | |||
386 | # Fascist default - deny access to everything. | ||
387 | <Directory /> | ||
388 | Options FollowSymLinks | ||
389 | AllowOverride None | ||
390 | ${allDenied} | ||
391 | </Directory> | ||
392 | |||
393 | # Generate directives for the main server. | ||
394 | ${perServerConf true mainCfg} | ||
395 | |||
396 | # Always enable virtual hosts; it doesn't seem to hurt. | ||
397 | ${let | ||
398 | listen = concatMap getListen allHosts; | ||
399 | uniqueListen = uniqList {inputList = listen;}; | ||
400 | directives = concatMapStrings (listen: "NameVirtualHost ${listenToString listen}\n") uniqueListen; | ||
401 | in optionalString (!version24) directives | ||
402 | } | ||
403 | |||
404 | ${let | ||
405 | makeVirtualHost = vhost: '' | ||
406 | <VirtualHost ${concatStringsSep " " (map listenToString (getListen vhost))}> | ||
407 | ${perServerConf false vhost} | ||
408 | </VirtualHost> | ||
409 | ''; | ||
410 | in concatMapStrings makeVirtualHost mainCfg.virtualHosts | ||
411 | } | ||
412 | ''; | ||
413 | |||
414 | |||
415 | enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices; | ||
416 | |||
417 | enablePerl = mainCfg.enablePerl || any (svc: svc.enablePerl) allSubservices; | ||
418 | |||
419 | |||
420 | # Generate the PHP configuration file. Should probably be factored | ||
421 | # out into a separate module. | ||
422 | phpIni = pkgs.runCommand "php.ini" | ||
423 | { options = concatStringsSep "\n" | ||
424 | ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices)); | ||
425 | preferLocalBuild = true; | ||
426 | } | ||
427 | '' | ||
428 | cat ${php}/etc/php.ini > $out | ||
429 | echo "$options" >> $out | ||
430 | ''; | ||
431 | |||
432 | in | ||
433 | |||
434 | |||
435 | { | ||
436 | |||
437 | ###### interface | ||
438 | |||
439 | options = { | ||
440 | |||
441 | services.httpd."${httpdName}" = { | ||
442 | |||
443 | enable = mkOption { | ||
444 | type = types.bool; | ||
445 | default = false; | ||
446 | description = "Whether to enable the Apache HTTP Server."; | ||
447 | }; | ||
448 | |||
449 | package = mkOption { | ||
450 | type = types.package; | ||
451 | default = pkgs.apacheHttpd; | ||
452 | defaultText = "pkgs.apacheHttpd"; | ||
453 | description = '' | ||
454 | Overridable attribute of the Apache HTTP Server package to use. | ||
455 | ''; | ||
456 | }; | ||
457 | |||
458 | configFile = mkOption { | ||
459 | type = types.path; | ||
460 | default = confFile; | ||
461 | defaultText = "confFile"; | ||
462 | example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."''; | ||
463 | description = '' | ||
464 | Override the configuration file used by Apache. By default, | ||
465 | NixOS generates one automatically. | ||
466 | ''; | ||
467 | }; | ||
468 | |||
469 | extraConfig = mkOption { | ||
470 | type = types.lines; | ||
471 | default = ""; | ||
472 | description = '' | ||
473 | Cnfiguration lines appended to the generated Apache | ||
474 | configuration file. Note that this mechanism may not work | ||
475 | when <option>configFile</option> is overridden. | ||
476 | ''; | ||
477 | }; | ||
478 | |||
479 | extraModules = mkOption { | ||
480 | type = types.listOf types.unspecified; | ||
481 | default = []; | ||
482 | example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]''; | ||
483 | description = '' | ||
484 | Additional Apache modules to be used. These can be | ||
485 | specified as a string in the case of modules distributed | ||
486 | with Apache, or as an attribute set specifying the | ||
487 | <varname>name</varname> and <varname>path</varname> of the | ||
488 | module. | ||
489 | ''; | ||
490 | }; | ||
491 | |||
492 | logPerVirtualHost = mkOption { | ||
493 | type = types.bool; | ||
494 | default = false; | ||
495 | description = '' | ||
496 | If enabled, each virtual host gets its own | ||
497 | <filename>access.log</filename> and | ||
498 | <filename>error.log</filename>, namely suffixed by the | ||
499 | <option>hostName</option> of the virtual host. | ||
500 | ''; | ||
501 | }; | ||
502 | |||
503 | user = mkOption { | ||
504 | type = types.str; | ||
505 | default = "wwwrun"; | ||
506 | description = '' | ||
507 | User account under which httpd runs. The account is created | ||
508 | automatically if it doesn't exist. | ||
509 | ''; | ||
510 | }; | ||
511 | |||
512 | group = mkOption { | ||
513 | type = types.str; | ||
514 | default = "wwwrun"; | ||
515 | description = '' | ||
516 | Group under which httpd runs. The account is created | ||
517 | automatically if it doesn't exist. | ||
518 | ''; | ||
519 | }; | ||
520 | |||
521 | logDir = mkOption { | ||
522 | type = types.path; | ||
523 | default = "/var/log/httpd"; | ||
524 | description = '' | ||
525 | Directory for Apache's log files. It is created automatically. | ||
526 | ''; | ||
527 | }; | ||
528 | |||
529 | stateDir = mkOption { | ||
530 | type = types.path; | ||
531 | default = "/run/httpd"; | ||
532 | description = '' | ||
533 | Directory for Apache's transient runtime state (such as PID | ||
534 | files). It is created automatically. Note that the default, | ||
535 | <filename>/run/httpd</filename>, is deleted at boot time. | ||
536 | ''; | ||
537 | }; | ||
538 | |||
539 | virtualHosts = mkOption { | ||
540 | type = types.listOf (types.submodule ( | ||
541 | { options = import <nixpkgs/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix> { | ||
542 | inherit lib; | ||
543 | forMainServer = false; | ||
544 | }; | ||
545 | })); | ||
546 | default = []; | ||
547 | example = [ | ||
548 | { hostName = "foo"; | ||
549 | documentRoot = "/data/webroot-foo"; | ||
550 | } | ||
551 | { hostName = "bar"; | ||
552 | documentRoot = "/data/webroot-bar"; | ||
553 | } | ||
554 | ]; | ||
555 | description = '' | ||
556 | Specification of the virtual hosts served by Apache. Each | ||
557 | element should be an attribute set specifying the | ||
558 | configuration of the virtual host. The available options | ||
559 | are the non-global options permissible for the main host. | ||
560 | ''; | ||
561 | }; | ||
562 | |||
563 | enableMellon = mkOption { | ||
564 | type = types.bool; | ||
565 | default = false; | ||
566 | description = "Whether to enable the mod_auth_mellon module."; | ||
567 | }; | ||
568 | |||
569 | enablePHP = mkOption { | ||
570 | type = types.bool; | ||
571 | default = false; | ||
572 | description = "Whether to enable the PHP module."; | ||
573 | }; | ||
574 | |||
575 | phpPackage = mkOption { | ||
576 | type = types.package; | ||
577 | default = pkgs.php; | ||
578 | defaultText = "pkgs.php"; | ||
579 | description = '' | ||
580 | Overridable attribute of the PHP package to use. | ||
581 | ''; | ||
582 | }; | ||
583 | |||
584 | enablePerl = mkOption { | ||
585 | type = types.bool; | ||
586 | default = false; | ||
587 | description = "Whether to enable the Perl module (mod_perl)."; | ||
588 | }; | ||
589 | |||
590 | phpOptions = mkOption { | ||
591 | type = types.lines; | ||
592 | default = ""; | ||
593 | example = | ||
594 | '' | ||
595 | date.timezone = "CET" | ||
596 | ''; | ||
597 | description = | ||
598 | "Options appended to the PHP configuration file <filename>php.ini</filename>."; | ||
599 | }; | ||
600 | |||
601 | multiProcessingModule = mkOption { | ||
602 | type = types.str; | ||
603 | default = "prefork"; | ||
604 | example = "worker"; | ||
605 | description = | ||
606 | '' | ||
607 | Multi-processing module to be used by Apache. Available | ||
608 | modules are <literal>prefork</literal> (the default; | ||
609 | handles each request in a separate child process), | ||
610 | <literal>worker</literal> (hybrid approach that starts a | ||
611 | number of child processes each running a number of | ||
612 | threads) and <literal>event</literal> (a recent variant of | ||
613 | <literal>worker</literal> that handles persistent | ||
614 | connections more efficiently). | ||
615 | ''; | ||
616 | }; | ||
617 | |||
618 | maxClients = mkOption { | ||
619 | type = types.int; | ||
620 | default = 150; | ||
621 | example = 8; | ||
622 | description = "Maximum number of httpd processes (prefork)"; | ||
623 | }; | ||
624 | |||
625 | maxRequestsPerChild = mkOption { | ||
626 | type = types.int; | ||
627 | default = 0; | ||
628 | example = 500; | ||
629 | description = | ||
630 | "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited"; | ||
631 | }; | ||
632 | |||
633 | sslCiphers = mkOption { | ||
634 | type = types.str; | ||
635 | default = "HIGH:!aNULL:!MD5:!EXP"; | ||
636 | description = "Cipher Suite available for negotiation in SSL proxy handshake."; | ||
637 | }; | ||
638 | |||
639 | sslProtocols = mkOption { | ||
640 | type = types.str; | ||
641 | default = "All -SSLv2 -SSLv3 -TLSv1"; | ||
642 | example = "All -SSLv2 -SSLv3"; | ||
643 | description = "Allowed SSL/TLS protocol versions."; | ||
644 | }; | ||
645 | } | ||
646 | |||
647 | # Include the options shared between the main server and virtual hosts. | ||
648 | // (import <nixpkgs/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix> { | ||
649 | inherit lib; | ||
650 | forMainServer = true; | ||
651 | }); | ||
652 | |||
653 | }; | ||
654 | |||
655 | |||
656 | ###### implementation | ||
657 | |||
658 | config = mkIf config.services.httpd."${httpdName}".enable { | ||
659 | |||
660 | assertions = [ { assertion = mainCfg.enableSSL == true | ||
661 | -> mainCfg.sslServerCert != null | ||
662 | && mainCfg.sslServerKey != null; | ||
663 | message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; } | ||
664 | ]; | ||
665 | |||
666 | warnings = map (cfg: ''apache-httpd's port option is deprecated. Use listen = [{/*ip = "*"; */ port = ${toString cfg.port};}]; instead'' ) (lib.filter (cfg: cfg.port != 0) allHosts); | ||
667 | |||
668 | users.users = optionalAttrs (withUsers && mainCfg.user == "wwwrun") (singleton | ||
669 | { name = "wwwrun"; | ||
670 | group = mainCfg.group; | ||
671 | description = "Apache httpd user"; | ||
672 | uid = config.ids.uids.wwwrun; | ||
673 | }); | ||
674 | |||
675 | users.groups = optionalAttrs (withUsers && mainCfg.group == "wwwrun") (singleton | ||
676 | { name = "wwwrun"; | ||
677 | gid = config.ids.gids.wwwrun; | ||
678 | }); | ||
679 | |||
680 | environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices; | ||
681 | |||
682 | services.httpd."${httpdName}".phpOptions = | ||
683 | '' | ||
684 | ; Needed for PHP's mail() function. | ||
685 | sendmail_path = sendmail -t -i | ||
686 | |||
687 | ; Don't advertise PHP | ||
688 | expose_php = off | ||
689 | '' + optionalString (!isNull config.time.timeZone) '' | ||
690 | |||
691 | ; Apparently PHP doesn't use $TZ. | ||
692 | date.timezone = "${config.time.timeZone}" | ||
693 | ''; | ||
694 | |||
695 | systemd.services."httpd${httpdName}" = | ||
696 | { description = "Apache HTTPD"; | ||
697 | |||
698 | wantedBy = [ "multi-user.target" ]; | ||
699 | wants = [ "keys.target" ]; | ||
700 | after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ]; | ||
701 | |||
702 | path = | ||
703 | [ httpd pkgs.coreutils pkgs.gnugrep ] | ||
704 | ++ optional enablePHP pkgs.system-sendmail # Needed for PHP's mail() function. | ||
705 | ++ concatMap (svc: svc.extraServerPath) allSubservices; | ||
706 | |||
707 | environment = | ||
708 | optionalAttrs enablePHP { PHPRC = phpIni; } | ||
709 | // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; } | ||
710 | // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices)); | ||
711 | |||
712 | preStart = | ||
713 | '' | ||
714 | mkdir -m 0750 -p ${mainCfg.stateDir} | ||
715 | [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir} | ||
716 | ${optionalString version24 '' | ||
717 | mkdir -m 0750 -p "${mainCfg.stateDir}/runtime" | ||
718 | [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime" | ||
719 | ''} | ||
720 | mkdir -m 0700 -p ${mainCfg.logDir} | ||
721 | |||
722 | # Get rid of old semaphores. These tend to accumulate across | ||
723 | # server restarts, eventually preventing it from restarting | ||
724 | # successfully. | ||
725 | for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do | ||
726 | ${pkgs.utillinux}/bin/ipcrm -s $i | ||
727 | done | ||
728 | |||
729 | # Run the startup hooks for the subservices. | ||
730 | for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do | ||
731 | echo Running Apache startup hook $i... | ||
732 | $i | ||
733 | done | ||
734 | ''; | ||
735 | |||
736 | serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}"; | ||
737 | serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop"; | ||
738 | serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful"; | ||
739 | serviceConfig.Type = "forking"; | ||
740 | serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid"; | ||
741 | serviceConfig.Restart = "always"; | ||
742 | serviceConfig.RestartSec = "5s"; | ||
743 | }; | ||
744 | |||
745 | }; | ||
746 | } | ||
diff --git a/modules/websites/nosslVhost/index.html b/modules/websites/nosslVhost/index.html new file mode 100644 index 00000000..4401a806 --- /dev/null +++ b/modules/websites/nosslVhost/index.html | |||
@@ -0,0 +1,11 @@ | |||
1 | <!DOCTYPE html> | ||
2 | <html> | ||
3 | <head> | ||
4 | <title>No SSL site</title> | ||
5 | </head> | ||
6 | <body> | ||
7 | <h1>No SSL on this site</h1> | ||
8 | <p>Use for wifi networks with login page that doesn't work well with | ||
9 | https.</p> | ||
10 | </body> | ||
11 | </html> | ||