summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/default.nix13
-rw-r--r--modules/myids.nix22
-rw-r--r--modules/secrets.nix61
-rw-r--r--modules/webapps/diaspora.nix171
-rw-r--r--modules/webapps/etherpad-lite.nix158
-rw-r--r--modules/webapps/mastodon.nix223
-rw-r--r--modules/webapps/mediagoblin.nix237
-rw-r--r--modules/webapps/peertube.nix105
-rw-r--r--modules/webapps/webstats/default.nix81
-rw-r--r--modules/webapps/webstats/goaccess.conf99
-rw-r--r--modules/websites/default.nix199
-rw-r--r--modules/websites/httpd-service-builder.nix746
-rw-r--r--modules/websites/nosslVhost/index.html11
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, ... }:
2let
3 name = "diaspora";
4 cfg = config.services.diaspora;
5
6 uid = config.ids.uids.diaspora;
7 gid = config.ids.gids.diaspora;
8in
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, ... }:
2let
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;
8in
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, ... }:
2let
3 name = "mastodon";
4 cfg = config.services.mastodon;
5
6 uid = config.ids.uids.mastodon;
7 gid = config.ids.gids.mastodon;
8in
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, ... }:
2let
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 '';
53in
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, ... }:
2let
3 name = "peertube";
4 cfg = config.services.peertube;
5
6 uid = config.ids.uids.peertube;
7 gid = config.ids.gids.peertube;
8in
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, ... }:
2let
3 name = "goaccess";
4 cfg = config.services.webstats;
5in {
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 @@
1time-format %H:%M:%S
2date-format %d/%b/%Y
3
4#sur immae.eu
5#log-format %v %h %^[%d:%t %^] "%r" %s %b "%R" "%u" $^
6
7log-format VCOMBINED
8#= %v:%^ %h %^[%d:%t %^] "%r" %s %b "%R" "%u"
9
10html-prefs {"theme":"bright","layout":"vertical"}
11
12exclude-ip 188.165.209.148
13exclude-ip 178.33.252.96
14exclude-ip 2001:41d0:2:9c94::1
15exclude-ip 2001:41d0:2:9c94::
16exclude-ip 176.9.151.89
17exclude-ip 2a01:4f8:160:3445::
18exclude-ip 82.255.56.72
19
20no-query-string true
21
22keep-db-files true
23load-from-disk true
24db-path @dbPath@
25
26ignore-panel REFERRERS
27ignore-panel KEYPHRASES
28
29static-file .css
30static-file .js
31static-file .jpg
32static-file .png
33static-file .gif
34static-file .ico
35static-file .jpeg
36static-file .pdf
37static-file .csv
38static-file .mpeg
39static-file .mpg
40static-file .swf
41static-file .woff
42static-file .woff2
43static-file .xls
44static-file .xlsx
45static-file .doc
46static-file .docx
47static-file .ppt
48static-file .pptx
49static-file .txt
50static-file .zip
51static-file .ogg
52static-file .mp3
53static-file .mp4
54static-file .exe
55static-file .iso
56static-file .gz
57static-file .rar
58static-file .svg
59static-file .bmp
60static-file .tar
61static-file .tgz
62static-file .tiff
63static-file .tif
64static-file .ttf
65static-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;
2let
3 cfg = config.services.websites;
4in
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
6with lib;
7
8let
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
432in
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>