├── LICENSE
├── README.md
├── default.nix
├── documentation
├── logo
│ ├── nixcloud.TLS.png
│ ├── nixcloud.TLS.svg
│ ├── nixcloud.container.png
│ ├── nixcloud.container.svg
│ ├── nixcloud.email.png
│ ├── nixcloud.email.svg
│ ├── nixcloud.webservices.png
│ └── nixcloud.webservices.svg
├── nixcloud-webservices.svg
├── nixcloud-webservices.svg.png
├── nixcloud.TLS.md
├── nixcloud.container.md
├── nixcloud.email.md
├── nixcloud.reverse-proxy.md
├── nixcloud.webservices-stateful-webservices-example.md
├── nixcloud.webservices.development.rst
└── nixcloud.webservices.md
├── lib
├── call-test.nix
├── hash-options
│ ├── default.nix
│ └── test.nix
├── make-toplevel-config.nix
└── make-webservice.nix
├── modules
├── core
│ ├── dbshell
│ │ ├── dbshell.py
│ │ └── default.nix
│ ├── directories
│ │ ├── default.nix
│ │ ├── lib.nix
│ │ ├── mkservices.nix
│ │ └── options.nix
│ ├── hashed-modules.nix
│ ├── ip2unix.nix
│ ├── packages.nix
│ ├── rspamd
│ │ ├── rspamd-1.7.3-local-rules.patch
│ │ ├── rspamd-1.7.9-local-rules.patch
│ │ └── rspamd-1.8.0-local-rules.patch
│ ├── testing.nix
│ ├── utils.nix
│ └── version.nix
├── default.nix
├── services
│ ├── TLS
│ │ ├── cert-options.nix
│ │ ├── common.nix
│ │ ├── default.nix
│ │ ├── module1.nix
│ │ ├── module2.nix
│ │ └── test.nix
│ ├── email
│ │ ├── dovecot
│ │ │ ├── filter_bin
│ │ │ │ └── rspamd.sh
│ │ │ ├── imap_sieve
│ │ │ │ ├── report-ham.sieve
│ │ │ │ └── report-spam.sieve
│ │ │ ├── pipe_bin
│ │ │ │ ├── learn-ham.sh
│ │ │ │ └── learn-spam.sh
│ │ │ └── sieve
│ │ │ │ ├── file-spam.sieve
│ │ │ │ └── rspamd.sieve
│ │ ├── nixcloud-email.nix
│ │ ├── opendkim.nix
│ │ ├── pfix-srsd.nix
│ │ ├── postfix.nix
│ │ ├── rspamd.nix
│ │ ├── rspamd
│ │ │ ├── groups.conf
│ │ │ ├── milter_headers.conf
│ │ │ ├── rspamd.local.lua
│ │ │ ├── settings.conf
│ │ │ └── statistic.conf
│ │ ├── test
│ │ │ ├── accounts.nix
│ │ │ ├── default.nix
│ │ │ └── test.py
│ │ ├── virtual-mail-submodule.nix
│ │ └── virtual-mail-users.nix
│ └── reverse-proxy
│ │ ├── default.nix
│ │ ├── options.nix
│ │ ├── test.nix
│ │ └── test
│ │ ├── config
│ │ ├── basicauth.nix
│ │ ├── blog.nix
│ │ ├── flubb.nix
│ │ ├── websocket.nix
│ │ └── wiki.nix
│ │ └── default.nix
├── virtualisation
│ └── container.nix
└── web
│ ├── core
│ ├── base.nix
│ ├── directories.nix
│ ├── meta.nix
│ └── webserver.nix
│ ├── database
│ ├── default.nix
│ ├── mysql-hook.sh
│ ├── mysql.nix
│ ├── postgresql-hook.sh
│ └── postgresql.nix
│ ├── default.nix
│ ├── messaging
│ ├── default.nix
│ └── rabbitmq
│ │ ├── default.nix
│ │ └── test.nix
│ ├── services
│ ├── apache
│ │ ├── default.nix
│ │ └── test.nix
│ ├── filesender
│ │ └── default.nix
│ ├── hydra
│ │ ├── default.nix
│ │ └── test.nix
│ ├── leaps
│ │ ├── default.nix
│ │ └── test.nix
│ ├── mattermost
│ │ ├── default.nix
│ │ └── test.nix
│ ├── mediawiki
│ │ ├── default.nix
│ │ └── test.nix
│ ├── nginx
│ │ ├── default.nix
│ │ └── test.nix
│ ├── roundcube
│ │ └── default.nix
│ ├── static-darkhttpd
│ │ ├── default.nix
│ │ └── test.nix
│ └── static-nginx
│ │ ├── default.nix
│ │ └── test.nix
│ └── webserver
│ ├── apache.nix
│ ├── darkhttpd.nix
│ ├── lib
│ ├── apache_check_config.nix
│ └── nginx_check_config.nix
│ ├── lighttpd.nix
│ └── nginx.nix
├── pkgs
├── default.nix
└── nixcloud
│ └── default.nix
├── release.nix
├── run-webservice.nix
├── run-webservice.sh
└── tests
├── common
└── eatmydata.nix
├── container.nix
├── custom-webservice.nix
├── dbshell.nix
├── default.nix
├── directories.nix
├── ip2unix.nix
├── mkunique.nix
├── user-allocation-uid-gid-test.nix
└── version.nix
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017 Joachim Schiele, Paul Seitz and the Nixcloud contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 | ======================================================================
23 |
24 | Note: the license above does not apply to the packages built by the
25 | Nix Packages collection, merely to the package descriptions (i.e., Nix
26 | expressions, build scripts, etc.). It also might not apply to patches
27 | included in Nixpkgs, which may be derivative works of the packages to
28 | which they apply. The aforementioned artifacts are all covered by the
29 | licenses of the respective packages.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | # ⚠️ This project is currently unmaintained
5 |
6 | If there is interest / funding, we can continue with this. Please contact js@lastlog.de for more.
7 |
8 | # nixcloud-webservices
9 |
10 | This [nixpkgs](https://github.com/NixOS/nixpkgs) extension, called [nixcloud-webservices](https://github.com/nixcloud/nixcloud-webservices), focuses on ease of deployment of web-related technologies.
11 |
12 | You should continue to read one of these documentations:
13 |
14 | * [nixcloud.webservices](documentation/nixcloud.webservices.md)
15 | * [nixcloud.reverse-proxy](documentation/nixcloud.reverse-proxy.md)
16 | * [nixcloud.email](documentation/nixcloud.email.md)
17 | * [nixcloud.TLS](documentation/nixcloud.TLS.md)
18 | * [nixcloud.container](https://github.com/nixcloud/nixcloud-container)
19 |
20 |
21 | It features the development stack we use at [https://nixcloud.io](https://nixcloud.io).
22 |
23 | Continuous integration at
24 |
25 | # Get the source
26 |
27 | Alternatively if you want to hack on nixcloud-webservices, you can also Git
28 | clone it with:
29 |
30 | ```sh-session
31 | $ git clone https://github.com/nixcloud/nixcloud-webservices
32 | ```
33 |
34 | Note: We no longer support pre-compiled binaries so you have to use the 'Get the source' workflow instead of using 'nix-channel'
35 |
36 | # CI hydra build
37 |
38 | https://headcounter.org/hydra/project/nixcloud-webservices
39 |
40 | # Importing
41 |
42 | ## Option A: Importing modules in your local system
43 |
44 | You import modules into your local system by adding the path to your `configuration.nix` to the `imports` list. Like this:
45 |
46 | ```nix
47 | {
48 | imports = [
49 | ./hardware-configuration.nix
50 | /path/to/nixcloud-webservices
51 | ];
52 | # ... other options ...
53 | }
54 | ```
55 |
56 | ## Option B: Building a (KVM) VM
57 |
58 | If you don't want to clutter your local system you can use a VM:
59 |
60 | nix-build '' --arg configuration '{ imports = [ ./modules ./config.nix ]; services.mingetty.autologinUser = "root"; }' -A vm
61 |
62 | Note: You have to create `config.nix` manually, it contains basically the lines we put in `/etc/nixos/configuration.nix` in previous examples.
63 |
64 | Note: This is for advanced users who know how VMs on NixOS work.
65 |
66 | # License
67 |
68 | The license can be found in [LICENSE](LICENSE).
69 |
70 | For inquiries, please contact:
71 |
72 | * Joachim Schiele
73 | * Paul Seitz
74 |
75 | # Thanks
76 |
77 | Many thanks to:
78 |
79 | - [https://www.internetsociety.org/](https://www.internetsociety.org/) (Sponsor)
80 | - [ISOC.nl](https://ISOC.nl) (Sponsor)
81 | - [Internet hardening fund](https://nlnet.nl/internethardening/) (Sponsor)
82 | - [profpatsch](https://github.com/Profpatsch) (Early prototyping)
83 | - [aszlig](https://github.com/aszlig) (NixOS module system)
84 | - [uwap](https://github.com/uwap) (Email abstraction)
85 | - [griff](https://github.com/griff) (Email abstraction)
86 | - [elias](https://github.com/eliasp) (nixcloud.TLS, email abstraction)
87 | - [clever](https://github.com/cleverca22) (Helping with hydra.nixcloud.io)
88 | - [brauner](https://github.com/brauner) (Help with LXC)
89 | - [leenaars](https://github.com/leenaars) (Requirements, testing, review)
90 | - [seitz](https://github.com/seitz)
91 | - [qknight](https://github.com/qknight)
92 |
93 | Among all who didn't make it into this list! Thanks for helping with writing this!
94 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | # This is only an alias for the main modules entry-point and it will be
2 | # replaced during channel creation. So if you ever need change anything in this
3 | # file, be sure to modify channel creation in release.nix as well.
4 | { imports = [ ./modules ]; }
5 |
--------------------------------------------------------------------------------
/documentation/logo/nixcloud.TLS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nixcloud/nixcloud-webservices/f2461802e5d80ad39c960ff018b5bb07b2adfd40/documentation/logo/nixcloud.TLS.png
--------------------------------------------------------------------------------
/documentation/logo/nixcloud.container.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nixcloud/nixcloud-webservices/f2461802e5d80ad39c960ff018b5bb07b2adfd40/documentation/logo/nixcloud.container.png
--------------------------------------------------------------------------------
/documentation/logo/nixcloud.email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nixcloud/nixcloud-webservices/f2461802e5d80ad39c960ff018b5bb07b2adfd40/documentation/logo/nixcloud.email.png
--------------------------------------------------------------------------------
/documentation/logo/nixcloud.webservices.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nixcloud/nixcloud-webservices/f2461802e5d80ad39c960ff018b5bb07b2adfd40/documentation/logo/nixcloud.webservices.png
--------------------------------------------------------------------------------
/documentation/nixcloud-webservices.svg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nixcloud/nixcloud-webservices/f2461802e5d80ad39c960ff018b5bb07b2adfd40/documentation/nixcloud-webservices.svg.png
--------------------------------------------------------------------------------
/documentation/nixcloud.container.md:
--------------------------------------------------------------------------------
1 | # nixcloud.container
2 |
3 | See
4 |
5 |
--------------------------------------------------------------------------------
/documentation/nixcloud.webservices-stateful-webservices-example.md:
--------------------------------------------------------------------------------
1 | # nixcloud.webservices 'Hack your service' example
2 |
3 | This usage, refered to as [Hack your service](nixcloud.webservices.md#hack-your-service), is a part of `nixcloud.webservices`. See also [../README.md](../README.md).
4 |
5 | ## Building the webpage using `styx`
6 |
7 | This is an example how to use `nixcloud.webservices.apache` with `styx` (or virtually any other static page generator) in combination to GIT and polling:
8 |
9 | * the webpage is built using 'styx'
10 | * the source is managed by GIT
11 | * cron properties are implemented with systemd's service/timer concept
12 | * the webpage is updated every 10 minutes
13 | * TLS is manged by `nixcloud.TLS`
14 |
15 | **Note:** Even though the webpage is generated statefully, the implementation is completely declarative.
16 |
17 | ### /etc/nixos/configuration.nix
18 |
19 | ...
20 | imports =
21 | [ # Include the results of the hardware scan.
22 | ./hardware-configuration.nix
23 |
24 | /etc/nixos/fractalide-website-styx.nix
25 | ];
26 | ...
27 | nixcloud.reverse-proxy.enable = true;
28 |
29 | ### /etc/nixos/fractalide-website-styx.nix
30 |
31 | { pkgs, config, lib, options, ... }:
32 | let
33 | buildAndSymlink = pkgs.writeScript "buildAndPublish-fractalide-website-styx.sh" ''
34 | #!${pkgs.stdenv.shell}
35 | source /etc/profile
36 | cd /var/lib/nixcloud/webservices/apache-fstyx/www
37 | if [ ! -f fractalide-website-styx ]; then
38 | ${pkgs.git}/bin/git clone https://github.com/fractalide/fractalide-website-styx
39 | fi
40 | cd fractalide-website-styx
41 | ${pkgs.git}/bin/git pull
42 | ${pkgs.nix}/bin/nix-build -o /var/lib/nixcloud/webservices/apache-fstyx/www/webpage
43 | '';
44 | in
45 | {
46 | nixcloud.webservices.apache.fstyx = {
47 | enable = true;
48 | proxyOptions = {
49 | domain = "fractalide.com";
50 | port = 8082;
51 | };
52 | webserver.apache = {
53 | extraConfig = ''
54 | DocumentRoot "/var/lib/nixcloud/webservices/apache-fstyx/www/webpage"
55 |
56 | DirectoryIndex index.html
57 | Options FollowSymLinks
58 | AllowOverride none
59 | Require all granted
60 |
61 | '';
62 | };
63 | };
64 | # systemctl list-timers --all
65 | systemd.timers.fstyx-timer = {
66 | description = "fractalide-website-styx trigger timer";
67 | wantedBy = [ "timers.target" ];
68 | timerConfig = {
69 | OnUnitActiveSec = "10min";
70 | OnBootSec = "2min";
71 | Unit = "fractalide-website-styx.service";
72 | Persistent = "yes";
73 | AccuracySec = "1m";
74 | RandomizedDelaySec = "1min";
75 | };
76 | };
77 | systemd.services.fractalide-website-styx = {
78 | after = [ "network.target" ];
79 | serviceConfig = {
80 | User = "apache-fstyx-webserver";
81 | Group = "apache-fstyx-webserver";
82 | ExecStart = buildAndSymlink;
83 | };
84 | };
85 | }
86 |
87 | **Note:** Make sure the User/Group name used with systemd is consistent with the webservice.
88 |
89 | ### Managing the service
90 |
91 | Once deployed using `nixos-rebuild switch`, check if the timer lists your service for updates:
92 |
93 | systemctl list-timers --all
94 | ...
95 | Fri 2018-06-01 14:48:27 UTC 4min 36s left Fri 2018-06-01 14:38:07 UTC 5min ago fstyx-timer.timer fractalide-website-styx.service
96 | ...
97 |
98 | If you want to rebuild manually:
99 |
100 | systemctl start fractalide-website-styx
101 |
102 | **Note:** Using systemd's service abstraction means we don't even have to maintain a lock file.
103 |
104 |
--------------------------------------------------------------------------------
/documentation/nixcloud.webservices.development.rst:
--------------------------------------------------------------------------------
1 | ***********************
2 | Writing service modules
3 | ***********************
4 |
5 | The basics for `writing NixOS modules`_ still apply here, but in order to
6 | support multiple instances, a few things are handled differently.
7 |
8 | Every service module is treated like a submodule and thus only has the options
9 | available within the submodule context. However, there is a special module
10 | attribute called ``toplevel``, which allows to access ``options`` and
11 | ``config`` from normal modules, like for example to access
12 | ``system.stateVersion`` the attribute to use is
13 | ``toplevel.config.system.stateVersion``.
14 |
15 | For passing option definitions back to the top-level, the same attribute exists
16 | within the ``config`` attribute of such a submodule.
17 |
18 | However there are cases in which using them should be avoided, because it would
19 | make those services unable to coexist with each other.
20 |
21 | Top-level options different in service modules
22 | ----------------------------------------------
23 |
24 | It's recommended to avoid these top-level options in service modules, because
25 | they interfere with the goal of having multiple instances.
26 |
27 | ``systemd``
28 | -----------
29 |
30 | The same option is available within the service modules, so simply don't use
31 | the ``toplevel.`` option for defining units.
32 |
33 | It differs from the top-level option such that it automatically gives the
34 | service name a unique prefix and orders the unit before
35 | ``instance-init.target`` (which itself is prefixed with the unique name of the
36 | service).
37 |
38 | Unit options such as ``before``, ``after``, ``wantedBy`` and other ordering
39 | options can be prefixed with an ``instance`` attribute to ensure they get
40 | prefixed versions.
41 |
42 | For example:
43 |
44 | .. code-block:: nix
45 |
46 | {
47 | systemd.services.foo = {
48 | wantedBy = [ "multi-user.target" ];
49 | };
50 |
51 | systemd.services.bar = {
52 | description = "Bar";
53 | instance.after = [ "foo.service" ];
54 | };
55 | }
56 |
57 | This will create two services ``foo`` and ``bar``, which both get their unique
58 | prefix, however there is ``wantedBy``, which is passed to the top-level without
59 | changes but ``instance.after`` passes ``foo.service`` with its instance prefix
60 | to the top-level.
61 |
62 | Another deviation from the top-level option is that the ``serviceConfig.User``
63 | and ``serviceConfig.Group`` options are automatically prefixed as well.
64 |
65 | ``users.users``
66 | ---------------
67 |
68 | This option is exposed to service modules as just ``users`` and it
69 | automatically prefixes the user name and the ``group`` attribute.
70 |
71 | ``users.groups``
72 | ----------------
73 |
74 | Same as with `users.users`_, but only the group name is prefixed.
75 |
76 | Other notable options
77 | ---------------------
78 |
79 | There are also some additional options available which are not part of the
80 | upstream NixOS modules. You can build a reference of these options by running
81 | ``nix-build release.nix -A manual`` from the root of this repository.
82 |
83 | Note that if you have systemd units defined that are dependant on a database
84 | being up, be sure to order them after ``database.target`` with
85 | ``instance.after``.
86 |
87 | ``database``
88 | ------------
89 |
90 | Allows to specify databases to use for this service. The attribute name is the
91 | database name and the value is another submodule which specifies
92 | database-specific options.
93 |
94 | Some options useful for module developers:
95 |
96 | ``socketPath``
97 | ^^^^^^^^^^^^^^
98 |
99 | This option is read-only and the module should always use the UNIX socket to
100 | communicate with the database. The reason for this is to avoid the need of
101 | passwords or certificates. Use of this option definition is only for module
102 | developers to find out the right socket path, not for users to expose.
103 |
104 | Depending on the application, either ``socketPath`` or ``phpHostname`` needs to
105 | be used in the connection string/function.
106 |
107 | ``phpHostname``
108 | ^^^^^^^^^^^^^^^
109 |
110 | Similar to `socketPath`_ but a useful helper for PHP-based web services,
111 | because database connections to sockets are specified differently in PHP
112 | dependending on the database type.
113 |
114 | The reason this is not called ``phpSocketPath`` is because for most PHP
115 | projects there is no easy way to specify the socket path, so this value is used
116 | to pass the socket path as a special host name.
117 |
118 | ``type``
119 | ^^^^^^^^
120 |
121 | The database type, which by default is the same as ``defaultDatabaseType``, so
122 | for a service module it should be only set if the application only supports one
123 | specific database type.
124 |
125 | ``user``
126 | ^^^^^^^^
127 |
128 | Specifies the user which will have access to the database determined by the
129 | user who connects to the UNIX socket (under the hood it's done by looking up
130 | SO_PEERCRED [1]_).
131 |
132 | ``postCreate``
133 | ^^^^^^^^^^^^^^
134 |
135 | Shell script to run directly after the database was created, which allows a
136 | special ``sqlsh`` command, which reads SQL commands from standard input and
137 | executes it on the right database.
138 |
139 | ``directories``
140 | ---------------
141 |
142 | This option is also available as a top-level option and it's there to create
143 | directories prior to service startup (which is a fairly common task to do).
144 | The option also exist within the service modules, but instead of specifying
145 | absolute paths, all paths are relative to the ``stateDir`` of the service
146 | module.
147 |
148 | ``tests.wanted``
149 | ----------------
150 |
151 | Available at the top-level as well with the same functionality, which is a list
152 | of test expressions for NixOS VM tests that need to pass once this service is
153 | enabled.
154 |
155 | Path options
156 | ------------
157 |
158 | There are two options, ``stateDir`` and ``runtimeDir``, which are both
159 | read-only and meant for module developers to reference the right path for the
160 | instance with its unique directory.
161 |
162 | The difference between those two is that ``runtimeDir`` is for files that
163 | do not persist and are only temporary (like sockets), while ``stateDir``
164 | contains all the data that should persist after restarts or reboots.
165 |
166 | Other helpers
167 | -------------
168 |
169 | The unique prefix for the service module is exposed via the ``uniqueName``
170 | option and there is another helper function that is passed to all modules as an
171 | argument called ``mkUnique``, which prepends the unique name in front of the
172 | string passed to it while removing duplicates (like eg.
173 | ``uniquename-uniquename-foo``).
174 |
175 | There are also variations of ``mkUnique``, one being ``mkUniqueUser`` and
176 | ``mkUniqueGroup`` which are for generating unique names for users and groups
177 | respectively. The reason why this differs from ``mkUnique`` is that user and
178 | group names are limited in length, so we need to hash them if they exceed 32
179 | characters.
180 |
181 | .. [1] Look into the `socket(7)`_ manpage in section ``Socket options`` for
182 | more information.
183 | .. _writing NixOS modules: https://nixos.org/nixos/manual/index.html#sec-writing-modules
184 | .. _socket(7): http://man7.org/linux/man-pages/man7/socket.7.html
185 |
--------------------------------------------------------------------------------
/lib/call-test.nix:
--------------------------------------------------------------------------------
1 | { system, pkgs, nixpkgs ? pkgs.path, ... }@args: test:
2 |
3 | let
4 | testLib = let
5 | mainExpr = import "${toString nixpkgs}/nixos/lib/testing.nix";
6 | funArgs = builtins.functionArgs mainExpr;
7 | attrs = { inherit system; } // lib.optionalAttrs (funArgs ? pkgs) {
8 | inherit pkgs;
9 | };
10 | in mainExpr attrs;
11 |
12 | inherit (pkgs) lib;
13 |
14 | getRelativePathStr = path: let
15 | root = toString ./..;
16 | in lib.removePrefix "/" (lib.removePrefix root (toString path));
17 |
18 | unitTest = pkgs.stdenv.mkDerivation {
19 | name = "unit-test-${testArgs.name}";
20 | buildInputs = [ pkgs.nix pkgs.jq ];
21 |
22 | NIX_PATH = "nixpkgs=${nixpkgs}:root=${lib.cleanSource ./..}";
23 |
24 | testExpr = let
25 | testPath = "";
26 | pkgsPath = "";
27 | in ''
28 | let
29 | pkgs = import {};
30 |
31 | reduceTestFun = tf: tf {
32 | pkgs = pkgs // {
33 | inherit (import ${pkgsPath} { inherit pkgs; }) nixcloud;
34 | };
35 | };
36 |
37 | unpackTestFun = tf:
38 | if builtins.isFunction tf then reduceTestFun tf
39 | else if builtins.isAttrs tf then tf
40 | else unpackTestFun (import tf);
41 |
42 | tests = pkgs.lib.mapAttrs' (name: value: {
43 | name = "test_''${name}";
44 | inherit value;
45 | }) (unpackTestFun ${testPath}).tests;
46 |
47 | in (import ).runTests tests
48 | '';
49 |
50 | buildCommand = ''
51 | export TEST_ROOT="$(pwd)/test-tmp"
52 | export NIX_CONF_DIR="$TEST_ROOT/etc"
53 | export NIX_DB_DIR="$TEST_ROOT/db"
54 | export NIX_LOCALSTATE_DIR="$TEST_ROOT/var"
55 | export NIX_STATE_DIR="$TEST_ROOT/var/nix"
56 | export NIX_STORE_DIR="$TEST_ROOT/store"
57 | nix-store --init
58 |
59 | test_output="$(
60 | nix-instantiate --show-trace --eval --json --strict -E "$testExpr"
61 | )"
62 |
63 | eval "$(echo "$test_output" | jq -r '
64 | map(.name + ": expected '\'''" +
65 | (.expected | tostring) + "'\''' but got '\'''" +
66 | (.result | tostring) + "'\'''")
67 | | @sh "echo \(. | join("\n"))"
68 | ')"
69 |
70 | [ "$test_output" = '[]' ]
71 | touch $out
72 | '';
73 |
74 | passthru.test = unitTest;
75 | };
76 |
77 | vmTest = testLib.makeTest (removeAttrs testArgsWithCommon [ "type" ]);
78 |
79 | reduceTestFun = tf: tf (args // {
80 | pkgs = pkgs // {
81 | inherit (import ../pkgs { inherit pkgs; }) nixcloud;
82 | };
83 | inherit nixpkgs;
84 | });
85 |
86 | unpackTestFun = tf:
87 | if builtins.isFunction tf then reduceTestFun tf
88 | else if builtins.isAttrs tf then tf
89 | else unpackTestFun (import tf);
90 |
91 | testArgs = unpackTestFun test;
92 |
93 | nodes = testArgs.nodes or (if testArgs ? machine then {
94 | inherit (testArgs) machine;
95 | } else {});
96 |
97 | injectCommon = name: conf: {
98 | imports = [ ../modules ../tests/common/eatmydata.nix conf ];
99 | services.mingetty.autologinUser = "root";
100 | # We don't want to wait for the timeout on https://cache.nixos.org/.
101 | nix.binaryCaches = lib.mkOverride 90 [];
102 | nixcloud.tests.enable = false;
103 | # Do not ever send out requests to letsencrypt.org.
104 | nixcloud.TLS.testMode = lib.mkOverride 90 true;
105 | };
106 |
107 | testArgsWithCommon = removeAttrs testArgs [ "machine" ] // {
108 | nodes = lib.mapAttrs injectCommon nodes;
109 | # This is so that we have a custom error message once one of our VM tests
110 | # has failed.
111 | testScript = args: let
112 | inherit (testArgs) testScript;
113 | mkPerlStr = val: "'${lib.escape ["\\" "'"] val}'";
114 | isTestScriptFun = builtins.isFunction testScript;
115 | realScript = if isTestScriptFun then testScript args else testScript;
116 | in ''
117 | eval {
118 | ${realScript}
119 | };
120 | if (my $error = $@) {
121 | chomp $error;
122 | print STDERR
123 | "\x1b[1;31mThe nixcloud test '".${mkPerlStr testArgs.name}."' has"
124 | . " failed with error '$error' but in case the machine was too slow"
125 | . " (virtualized, not enough ram, too much cpu load, etc) then"
126 | . " you can also disable the tests by adding 'nixcloud.tests.enable"
127 | . " = false;' to your /etc/nixos/configuration.nix and still use"
128 | . " our software.\x1b[m\n";
129 | exit(1);
130 | }
131 | '';
132 | };
133 |
134 | in if (testArgs.type or "vm") == "unit" then unitTest else vmTest
135 |
--------------------------------------------------------------------------------
/lib/hash-options/default.nix:
--------------------------------------------------------------------------------
1 | # Recursively hashes all options so that it can be used for comparing a
2 | # specific option interface against another.
3 | { lib ? import }:
4 |
5 | let
6 | # Make sure that all the information about a type is present without the need
7 | # to evaluate lambdas (which we can't hash).
8 | cleanType = type: let
9 | inherit (type.functor) wrapped;
10 | in {
11 | inherit (type) name description;
12 | wrapped = lib.mapNullable cleanType wrapped;
13 | };
14 |
15 | # This recursively walks through all the options and submodules to generate a
16 | # structure that can be safely transformed into JSON.
17 | walk = path: opt: let
18 | sanitize = { type, ... }@attrs: {
19 | inherit path;
20 | default = attrs.default or null;
21 | type = cleanType type;
22 | subOptions = walk path (type.getSubOptions path);
23 | };
24 | recurse = name: walk (path ++ lib.singleton name);
25 | mapNested = lib.concatLists (lib.mapAttrsToList recurse opt);
26 | in if lib.isOption opt then lib.singleton (sanitize opt)
27 | else if lib.isAttrs opt then mapNested
28 | else [];
29 |
30 | hashAttrs = x: builtins.hashString "sha256" (builtins.toJSON x);
31 | hashOptions = x: hashAttrs (walk [] x);
32 |
33 | in hashOptions
34 |
--------------------------------------------------------------------------------
/lib/hash-options/test.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import {}, lib ? pkgs.lib }:
2 |
3 | let
4 | inherit (lib) types mkOption;
5 |
6 | defaultOption = mkOption {
7 | type = types.str;
8 | };
9 |
10 | hashOptions = import ./. { inherit lib; };
11 |
12 | makeTestCase = { descWhen, before, after, succeeds ? false }: let
13 | desc = if succeeds then "Hash is still the same but ${descWhen}"
14 | else "Hash is different when ${descWhen}";
15 | beforeHash = hashOptions before;
16 | afterHash = hashOptions after;
17 | success = if succeeds then beforeHash == afterHash
18 | else beforeHash != afterHash;
19 | operator = if succeeds then "!=" else "==";
20 | details = "${beforeHash} ${operator} ${afterHash}";
21 | info = "${desc}: ${if success then "OK" else "FAILED: ${details}"}";
22 | in builtins.trace info { inherit info success; };
23 |
24 | mkTests = cases: let
25 | results = lib.mapAttrsToList (lib.const makeTestCase) cases;
26 | allSucceed = lib.all (r: r.success) results;
27 | resultStr = lib.concatMapStringsSep "\n" (r: r.info) results;
28 | drvTrue = pkgs.writeText "tests-succeeded" "${resultStr}\n";
29 | in if allSucceed then drvTrue else throw "Tests failed.";
30 |
31 | in mkTests {
32 |
33 | exampleChanged = {
34 | descWhen = "the example of the option has changed";
35 |
36 | before = mkOption {
37 | type = types.str;
38 | default = "foo";
39 | example = "bar";
40 | };
41 |
42 | after = mkOption {
43 | type = types.str;
44 | default = "foo";
45 | example = "different";
46 | };
47 |
48 | succeeds = true;
49 | };
50 |
51 | defaultChanged = {
52 | descWhen = "the default value of the option has changed";
53 |
54 | before = mkOption {
55 | type = types.str;
56 | default = "foo";
57 | };
58 |
59 | after = mkOption {
60 | type = types.str;
61 | default = "bar";
62 | };
63 | };
64 |
65 | strChangedToLines = {
66 | descWhen = "the type has changed and it's also a str-like type";
67 |
68 | before = mkOption {
69 | type = types.str;
70 | default = "123";
71 | };
72 |
73 | after = mkOption {
74 | type = types.lines;
75 | default = "123";
76 | };
77 | };
78 |
79 | nestedInNullOr = {
80 | descWhen = "a nested type has changed";
81 |
82 | before = mkOption {
83 | type = types.nullOr types.str;
84 | default = null;
85 | };
86 |
87 | after = mkOption {
88 | type = types.nullOr types.lines;
89 | default = null;
90 | };
91 | };
92 |
93 | renamedOption = {
94 | descWhen = "an option has been renamed";
95 |
96 | before.name1 = defaultOption;
97 | after.name2 = defaultOption;
98 | };
99 |
100 | relocatedOption = {
101 | descWhen = "an option has been relocated";
102 |
103 | before.originalLocation = defaultOption;
104 | after.new.location = defaultOption;
105 | };
106 |
107 | removeOption = {
108 | descWhen = "an option has been removed";
109 |
110 | before.someOption = defaultOption;
111 | after = {};
112 | };
113 |
114 | multipleUnchanged = {
115 | descWhen = "multiple options have changed";
116 |
117 | before = {
118 | opt1 = defaultOption;
119 | opt2 = defaultOption;
120 | };
121 |
122 | after = {
123 | opt1 = mkOption {
124 | type = types.attrs;
125 | };
126 | newOpt2 = defaultOption;
127 | };
128 | };
129 |
130 | submoduleDiffers = {
131 | descWhen = "a submodule's option has a different type";
132 |
133 | before = mkOption {
134 | type = types.submodule {
135 | options.foo = defaultOption;
136 | };
137 | };
138 |
139 | after = mkOption {
140 | type = types.submodule {
141 | options.foo = mkOption {
142 | type = types.listOf types.str;
143 | };
144 | };
145 | };
146 | };
147 |
148 | subsubmoduleDiffers = {
149 | descWhen = "a submodule's option of a submodule has a different type";
150 |
151 | before = mkOption {
152 | type = types.submodule {
153 | options.subsub = mkOption {
154 | type = types.submodule {
155 | options.foo = defaultOption;
156 | };
157 | };
158 | };
159 | };
160 |
161 | after = mkOption {
162 | type = types.submodule {
163 | options.subsub = mkOption {
164 | type = types.submodule {
165 | options.foo = mkOption {
166 | type = types.unspecified;
167 | };
168 | };
169 | };
170 | };
171 | };
172 | };
173 |
174 | coerced = {
175 | descWhen = "the to-be-coerced subtype has changed";
176 |
177 | before = mkOption {
178 | type = types.coercedTo types.int toString types.str;
179 | };
180 |
181 | after = mkOption {
182 | type = types.coercedTo (types.listOf types.int) toString types.str;
183 | };
184 | };
185 |
186 | coercedFinal = {
187 | descWhen = "the final subtype of a coercion has changed";
188 |
189 | before = mkOption {
190 | type = types.coercedTo types.int lib.id types.int;
191 | };
192 |
193 | after = mkOption {
194 | type = types.coercedTo types.int lib.id (types.nullOr types.int);
195 | };
196 | };
197 |
198 | enum = {
199 | descWhen = "values of an enum have changed";
200 |
201 | before = mkOption {
202 | type = types.enum [ "a" "b" "c" ];
203 | };
204 |
205 | after = mkOption {
206 | type = types.enum [ "a" "b" "x" ];
207 | };
208 | };
209 |
210 | defaultIsDerivation = {
211 | descWhen = "the default value is a derivation";
212 |
213 | before = mkOption {
214 | type = types.package;
215 | default = pkgs.writeText "foo" "foo";
216 | };
217 |
218 | after = mkOption {
219 | type = types.package;
220 | default = pkgs.writeText "bar" "bar";
221 | };
222 | };
223 | }
224 |
--------------------------------------------------------------------------------
/lib/make-toplevel-config.nix:
--------------------------------------------------------------------------------
1 | { config, options, lib }:
2 |
3 | # Generate configuration values by avoiding to trigger an infinite recursion
4 | # of the `config' fixpoint.
5 | #
6 | # The first argument is a function Config -> [AttrSet] which returns a list of
7 | # all the top-level definitions, like for example:
8 | #
9 | # [ { systemd.services.foo = { description = "bar"; ... }; }
10 | # { environment.etc.bar.source = ...; }
11 | # ]
12 | #
13 | # The second argument is a path of the configuration value to map the function
14 | # from the first argument on. For example if the path is ["foo" "bar"], the
15 | # function will be applied like this:
16 | #
17 | # map fun config.foo.bar
18 | #
19 | # We need this to be a path instead of a plain attribute, because we need to
20 | # look up the same path in options to figure out on which values we can't
21 | # recurse on (like the configuration value of that path itself).
22 | #
23 | fun: cfgval: let
24 | # These are the configuration paths we don't want to reference, because
25 | # if we do, we would trip the fixpoint and trigger an infinite
26 | # recursion.
27 | unsafePaths = [
28 | # For injecting args to modules, so it's a very obvious contender.
29 | ["_module"]
30 | # Here we have checkConfigurationOptions, which is an alias of
31 | # _module.check.
32 | ["environment"]
33 | # The common namespace for NixCloud.
34 | ["nixcloud"]
35 | ];
36 |
37 | # Put in all values of environment.* except checkConfigurationOptions.
38 | safePaths = let
39 | system = if config.nixpkgs ? localSystem then config.nixpkgs.localSystem.system else config.nixpkgs.system;
40 | reEval = import {
41 | inherit system;
42 | modules = [];
43 | check = false;
44 | };
45 | safeEnvOpts = lib.attrNames (removeAttrs reEval.options.environment [
46 | "checkConfigurationOptions"
47 | ]);
48 | in map (p: ["environment" p]) safeEnvOpts;
49 |
50 | mkThunks = acc: path: let
51 | inherit (builtins) attrNames head tail;
52 | # Get all singletons into right and everything else in wrong.
53 | inherit (lib.partition (e: builtins.length e == 1) acc) right wrong;
54 | # The flattened elements from the list of singletons.
55 | singletons = map head right;
56 | # Top-level attributes that can safely be included.
57 | top = removeAttrs (lib.getAttrFromPath path options) singletons;
58 | # All the leaves we no longer need to recurse into.
59 | leaves = map (n: path ++ [ n ]) (attrNames top);
60 | # However, we want to recurse into 'wrong' (non-leaves) so that we
61 | # have an attribute set of the first elements as names and a list of
62 | # all the successive elements as values.
63 | sorted = lib.zipAttrs (map (e: { ${head e} = tail e; }) wrong);
64 | # Recurse further into each node, appending the current node to path.
65 | recurse = node: rest: mkThunks rest (path ++ [ node ]);
66 | in leaves ++ builtins.concatLists (lib.mapAttrsToList recurse sorted);
67 |
68 | # Map over each list item `fun' returns and generate attribute setters
69 | # for each path that is *not* listed in unsafePaths.
70 | unbox = path: let
71 | foldThunks = defs: acc: let
72 | maybePath = lib.optional (lib.hasAttrByPath path defs);
73 | in acc ++ maybePath (lib.getAttrFromPath path defs);
74 | eval = fun (lib.getAttrFromPath cfgval config);
75 | in lib.setAttrByPath path (lib.mkMerge (lib.fold foldThunks [] eval));
76 |
77 | in lib.mkMerge (map unbox ((mkThunks unsafePaths []) ++ safePaths))
78 |
--------------------------------------------------------------------------------
/lib/make-webservice.nix:
--------------------------------------------------------------------------------
1 | wsName: module:
2 |
3 | { config, options, pkgs, lib, nclib, ... }@toplevel:
4 |
5 | {
6 | options.nixcloud.webservices.${wsName} = lib.mkOption {
7 | type = lib.types.attrsOf (lib.types.submodule {
8 | imports = [
9 | module
10 | ../modules/web/core/base.nix
11 | ../modules/web/webserver/apache.nix
12 | ../modules/web/webserver/lighttpd.nix
13 | ../modules/web/webserver/nginx.nix
14 | ../modules/web/webserver/darkhttpd.nix
15 | ];
16 | _module.args = { inherit wsName pkgs toplevel nclib; };
17 | });
18 | default = {};
19 | # TODO: Flesh out this description.
20 | description = "NixCloud ${wsName} web service definitions";
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/modules/core/dbshell/dbshell.py:
--------------------------------------------------------------------------------
1 | #!@interpreter@
2 | import os
3 | from pwd import getpwnam
4 | from argparse import ArgumentParser
5 |
6 | DBSHELL_CONFIG = @dbshellConfig@ # noqa
7 | WEBSERVICES_PREFIX = "/var/lib/nixcloud/webservices"
8 |
9 |
10 | def run_shell(dbname, user, command):
11 | os.setuid(getpwnam(user).pw_uid)
12 | os.execl(command, command, user, dbname)
13 |
14 |
15 | def determine_wsname():
16 | rel_to_ws_dir = os.path.relpath(os.getcwd(), WEBSERVICES_PREFIX)
17 | components = rel_to_ws_dir.split(os.sep, 1)
18 | if len(components) != 2:
19 | return None
20 | wsname_canidate = components[0]
21 | if wsname_canidate in [os.curdir, os.pardir]:
22 | return None
23 | if wsname_canidate not in DBSHELL_CONFIG:
24 | return None
25 | return wsname_canidate
26 |
27 |
28 | if __name__ == '__main__':
29 | desc = "Connect to a database within a web service instance"
30 | parser = ArgumentParser(description=desc)
31 | parser.add_argument("webservice_name", nargs='?',
32 | help="The web service name. If the argument is"
33 | " omitted, the service name is determined"
34 | " by inspecting the current directory.")
35 | parser.add_argument("database", help="The database name to connect to.")
36 | options = parser.parse_args()
37 |
38 | if options.webservice_name is None:
39 | wsname = determine_wsname()
40 | if wsname is None:
41 | parser.error("Unable to determine web service name.")
42 | elif options.webservice_name not in DBSHELL_CONFIG:
43 | msg = "Web service {!r} does not exist."
44 | parser.error(msg.format(options.webservice_name))
45 | else:
46 | wsname = options.webservice_name
47 |
48 | wsdef = DBSHELL_CONFIG[wsname]
49 | if options.database not in wsdef:
50 | msg = "Database {!r} does not exist for web service {!r}."
51 | parser.error(msg.format(options.database, wsname))
52 | else:
53 | run_shell(options.database, **wsdef[options.database])
54 |
--------------------------------------------------------------------------------
/modules/core/dbshell/default.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, nclib, ... }:
2 |
3 | let
4 | toPython = value: let
5 | mkStr = val: "'${lib.escape ["'" "\\"] val}'";
6 | mkDict = attrs: let
7 | mkEntry = key: val: "${mkStr key}: ${toPython val}";
8 | entries = lib.mapAttrsToList mkEntry attrs;
9 | in "{${lib.concatStringsSep "," entries}}";
10 | mkTuple = lib.concatMapStringsSep "," toPython;
11 | in if value == true then "True"
12 | else if value == false then "False"
13 | else if lib.isAttrs value then mkDict value
14 | else if lib.isString value then mkStr value
15 | else if lib.isInt value then toString value
16 | else if lib.isList value then mkTuple value
17 | else throw "Can't convert '${value}' into a Python value.";
18 |
19 | dbshellConfig = nclib.mapWSConfigToList (cfg: lib.optional cfg.enable {
20 | name = cfg.uniqueName;
21 | value = lib.mapAttrs' (lib.const (dbcfg: {
22 | inherit (dbcfg) name;
23 | value = {
24 | user = cfg._module.args.mkUniqueUser dbcfg.user;
25 | command = let
26 | inherit (dbcfg) type;
27 | errMsg = "Unable to determine database shell access command"
28 | + " for database type '${type}'.\n"
29 | + "Please define 'dbShellCommand' in your database module.";
30 | in toString (pkgs.writeScript "dbshell-${type}.sh" ''
31 | #!${pkgs.stdenv.shell} -e
32 | ${cfg.dbShellCommand.${type} or (throw errMsg)}
33 | '');
34 | };
35 | })) cfg.database;
36 | });
37 |
38 | wrapper = pkgs.runCommand "nixcloud-dbshell" {
39 | sourceFile = ./dbshell.py;
40 | dbshellConfig = toPython (lib.listToAttrs (lib.concatLists dbshellConfig));
41 | inherit (pkgs.python3Packages.python) interpreter;
42 | } ''
43 | mkdir -p "$out/bin"
44 | substituteAll "$sourceFile" "$out/bin/nixcloud-dbshell"
45 | chmod +x "$out/bin/nixcloud-dbshell"
46 | '';
47 |
48 | in {
49 | environment.systemPackages = [ wrapper ];
50 | }
51 |
--------------------------------------------------------------------------------
/modules/core/directories/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs, lib, config, ... }:
2 |
3 | {
4 | options.nixcloud.directories = import ./options.nix { inherit config lib; };
5 |
6 | config = lib.mkIf (config.nixcloud.directories != {}) {
7 | systemd.services = let
8 | serviceList = lib.mapAttrsToList (import ./mkservices.nix {
9 | inherit pkgs lib config;
10 | }) config.nixcloud.directories;
11 | in lib.listToAttrs (lib.concatLists serviceList);
12 |
13 | assertions = lib.concatLists (lib.mapAttrsToList (path: cfg: let
14 | optname = "nixcloud.directories.\"${path}\"";
15 | mkUserAssertion = where: username: {
16 | assertion = config.users.users ? ${username};
17 | message = "The user '${username}' specified in '${optname}.${where}'"
18 | + " does not exist.";
19 | };
20 | mkGroupAssertion = where: groupname: {
21 | assertion = config.users.groups ? ${groupname};
22 | message = "The group '${groupname}' specified in '${optname}.${where}'"
23 | + " does not exist.";
24 | };
25 | assertions = [
26 | (mkUserAssertion "owner" cfg.owner)
27 | (mkGroupAssertion "group" cfg.group)
28 | ] ++ map (mkUserAssertion "users") (lib.attrNames cfg.users)
29 | ++ map (mkGroupAssertion "groups") (lib.attrNames cfg.groups);
30 | in assertions) config.nixcloud.directories);
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/modules/core/directories/lib.nix:
--------------------------------------------------------------------------------
1 | { lib }:
2 |
3 | rec {
4 | # Returns a list of the components of the given path.
5 | splitPath = path: let
6 | result = builtins.match "([^/]*)/(.*)" path;
7 | iter = [ (lib.head result) ] ++ splitPath (lib.last result);
8 | in if result == null then [ path ] else iter;
9 |
10 | # Strips out every component that contains "." or ".." and also strips out
11 | # unnecessary slashes (eg. "////" becomes "/").
12 | sanitizePath = path: let
13 | splitted = splitPath path;
14 | filtered = lib.filter (c: c != "" && c != "." && c != "..") splitted;
15 | in lib.concatStringsSep "/" filtered;
16 |
17 | # Turn a permission config (created by mkPermOpts) into short text
18 | # permission form like eg. "rwx" or "r-x".
19 | permConfToRWX = isDir: cfg: let
20 | execAttr = if isDir then "search" else "execute";
21 | fullBits = if isDir then "rwx" else "rw${execBit}";
22 | mkBit = val: bit: if val then bit else "-";
23 | readBit = mkBit cfg.read "r";
24 | writeBit = mkBit cfg.write "w";
25 | execBit = mkBit cfg.${execAttr} "x";
26 | in if cfg.fullAccess then fullBits
27 | else if cfg.noAccess then "---"
28 | else readBit + writeBit + execBit;
29 | }
30 |
--------------------------------------------------------------------------------
/modules/core/hashed-modules.nix:
--------------------------------------------------------------------------------
1 | { config, lib, modulesPath, ... }:
2 |
3 | let
4 | inherit (lib) types mkOption;
5 | cfg = config.nixcloud.assertions;
6 |
7 | mkAssertion = path: expectedHash: let
8 | contents = builtins.readFile "${toString modulesPath}/${toString path}";
9 | hash = builtins.hashString "sha256" contents;
10 | in {
11 | assertion = hash == expectedHash;
12 | message = "Hash mismatch for `${path}': "
13 | + "Expected `${expectedHash}' but got `${hash}'.";
14 | };
15 |
16 | hashType = lib.mkOptionType {
17 | name = "sha256";
18 | description = "base-16 SHA-256 hash";
19 | check = val: lib.isString val
20 | && builtins.match "[a-fA-F0-9]{64}" val != null;
21 | merge = lib.mergeOneOption;
22 | };
23 |
24 | in {
25 | options.nixcloud.assertions.moduleHashes = mkOption {
26 | type = types.attrsOf hashType;
27 | default = {};
28 | example."services/mail/opendkim.nix" =
29 | "a937be8731e6e1a7b7872a2dc72274b4a31364f249bfcf8ef7bcc98753c9a018";
30 | description = ''
31 | An attribute set consisting of module paths relative to the NixOS module
32 | directory as attribute names and their corresponding SHA-256 hashes as
33 | attribute values.
34 | '';
35 | };
36 |
37 | config.assertions = lib.mapAttrsToList mkAssertion cfg.moduleHashes;
38 | }
39 |
--------------------------------------------------------------------------------
/modules/core/packages.nix:
--------------------------------------------------------------------------------
1 | { lib, pkgs, ... }:
2 | with pkgs;
3 |
4 | {
5 | nixpkgs.overlays = [
6 | (self: super: {
7 | inherit (import ../../pkgs { inherit pkgs; }) nixcloud;
8 | #lxc = super.lxc.overrideAttrs (drv: rec {
9 | # version = "2.1.1";
10 | # name = "lxc-${version}";
11 | # src = /etc/nixos/pkgs/lxc;
12 | # patches = [ ];
13 | #});
14 | rspamd = let
15 | version = super.rspamd.version;
16 | in if (lib.versionAtLeast version "1.7.3") && (lib.versionOlder version "1.8.1")
17 | then super.rspamd.overrideAttrs (oldAttrs: rec {
18 | patches = ["${./rspamd}/rspamd-${version}-local-rules.patch"];
19 | })
20 | else super.rspamd;
21 | })
22 | ];
23 | }
24 |
--------------------------------------------------------------------------------
/modules/core/rspamd/rspamd-1.7.3-local-rules.patch:
--------------------------------------------------------------------------------
1 | diff --git a/rules/rspamd.lua b/rules/rspamd.lua
2 | index 6b53828ee..0f7990d58 100644
3 | --- a/rules/rspamd.lua
4 | +++ b/rules/rspamd.lua
5 | @@ -21,7 +21,7 @@ require "global_functions" ()
6 | config['regexp'] = {}
7 | rspamd_maps = {} -- Global maps
8 |
9 | -local local_conf = rspamd_paths['CONFDIR']
10 | +local local_conf = rspamd_paths['LOCAL_CONFDIR']
11 | local local_rules = rspamd_paths['RULESDIR']
12 |
13 | dofile(local_rules .. '/regexp/headers.lua')
14 | @@ -74,4 +74,4 @@ if rmaps and type(rmaps) == 'table' then
15 | end
16 |
17 | local rspamd_nn = require "lua_nn"
18 | -rspamd_nn.load_rspamd_nn() -- Load defined models
19 | \ No newline at end of file
20 | +rspamd_nn.load_rspamd_nn() -- Load defined models
21 | diff --git a/src/libserver/cfg_rcl.c b/src/libserver/cfg_rcl.c
22 | index 6019d7c7d..0e17afa29 100644
23 | --- a/src/libserver/cfg_rcl.c
24 | +++ b/src/libserver/cfg_rcl.c
25 | @@ -612,6 +612,7 @@ rspamd_rcl_worker_handler (rspamd_mempool_t *pool, const ucl_object_t *obj,
26 | }
27 |
28 | #define RSPAMD_CONFDIR_INDEX "CONFDIR"
29 | +#define RSPAMD_LOCAL_CONFDIR_INDEX "LOCAL_CONFDIR"
30 | #define RSPAMD_RUNDIR_INDEX "RUNDIR"
31 | #define RSPAMD_DBDIR_INDEX "DBDIR"
32 | #define RSPAMD_LOGDIR_INDEX "LOGDIR"
33 | @@ -819,6 +820,7 @@ rspamd_rcl_set_lua_globals (struct rspamd_config *cfg, lua_State *L,
34 | lua_getglobal (L, "rspamd_paths");
35 | if (lua_isnil (L, -1)) {
36 | const gchar *confdir = RSPAMD_CONFDIR, *rundir = RSPAMD_RUNDIR,
37 | + *local_confdir = RSPAMD_LOCAL_CONFDIR,
38 | *dbdir = RSPAMD_DBDIR, *logdir = RSPAMD_LOGDIR,
39 | *wwwdir = RSPAMD_WWWDIR, *pluginsdir = RSPAMD_PLUGINSDIR,
40 | *rulesdir = RSPAMD_RULESDIR, *lualibdir = RSPAMD_LUALIBDIR,
41 | @@ -866,6 +868,11 @@ rspamd_rcl_set_lua_globals (struct rspamd_config *cfg, lua_State *L,
42 | confdir = t;
43 | }
44 |
45 | + t = getenv ("LOCAL_CONFDIR");
46 | + if (t) {
47 | + local_confdir = t;
48 | + }
49 | +
50 |
51 | if (vars) {
52 | t = g_hash_table_lookup (vars, "PLUGINSDIR");
53 | @@ -898,6 +905,11 @@ rspamd_rcl_set_lua_globals (struct rspamd_config *cfg, lua_State *L,
54 | confdir = t;
55 | }
56 |
57 | + t = g_hash_table_lookup (vars, "LOCAL_CONFDIR");
58 | + if (t) {
59 | + local_confdir = t;
60 | + }
61 | +
62 | t = g_hash_table_lookup (vars, "DBDIR");
63 | if (t) {
64 | dbdir = t;
65 | @@ -912,6 +924,7 @@ rspamd_rcl_set_lua_globals (struct rspamd_config *cfg, lua_State *L,
66 | lua_createtable (L, 0, 9);
67 |
68 | rspamd_lua_table_set (L, RSPAMD_CONFDIR_INDEX, confdir);
69 | + rspamd_lua_table_set (L, RSPAMD_LOCAL_CONFDIR_INDEX, local_confdir);
70 | rspamd_lua_table_set (L, RSPAMD_RUNDIR_INDEX, rundir);
71 | rspamd_lua_table_set (L, RSPAMD_DBDIR_INDEX, dbdir);
72 | rspamd_lua_table_set (L, RSPAMD_LOGDIR_INDEX, logdir);
73 |
--------------------------------------------------------------------------------
/modules/core/rspamd/rspamd-1.7.9-local-rules.patch:
--------------------------------------------------------------------------------
1 | diff --git a/rules/rspamd.lua b/rules/rspamd.lua
2 | index a193eb495..40b6af704 100644
3 | --- a/rules/rspamd.lua
4 | +++ b/rules/rspamd.lua
5 | @@ -21,7 +21,7 @@ require "global_functions" ()
6 | config['regexp'] = {}
7 | rspamd_maps = {} -- Global maps
8 |
9 | -local local_conf = rspamd_paths['CONFDIR']
10 | +local local_conf = rspamd_paths['LOCAL_CONFDIR']
11 | local local_rules = rspamd_paths['RULESDIR']
12 | local rspamd_util = require "rspamd_util"
13 |
14 | diff --git a/src/lua/lua_common.c b/src/lua/lua_common.c
15 | index 323957086..1e4538b1c 100644
16 | --- a/src/lua/lua_common.c
17 | +++ b/src/lua/lua_common.c
18 | @@ -576,6 +576,7 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
19 | lua_getglobal (L, "rspamd_paths");
20 | if (lua_isnil (L, -1)) {
21 | const gchar *confdir = RSPAMD_CONFDIR, *rundir = RSPAMD_RUNDIR,
22 | + *local_confdir = RSPAMD_LOCAL_CONFDIR,
23 | *dbdir = RSPAMD_DBDIR, *logdir = RSPAMD_LOGDIR,
24 | *wwwdir = RSPAMD_WWWDIR, *pluginsdir = RSPAMD_PLUGINSDIR,
25 | *rulesdir = RSPAMD_RULESDIR, *lualibdir = RSPAMD_LUALIBDIR,
26 | @@ -623,6 +624,11 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
27 | confdir = t;
28 | }
29 |
30 | + t = getenv ("LOCAL_CONFDIR");
31 | + if (t) {
32 | + local_confdir = t;
33 | + }
34 | +
35 |
36 | if (vars) {
37 | t = g_hash_table_lookup (vars, "PLUGINSDIR");
38 | @@ -655,6 +661,11 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
39 | confdir = t;
40 | }
41 |
42 | + t = g_hash_table_lookup (vars, "LOCAL_CONFDIR");
43 | + if (t) {
44 | + local_confdir = t;
45 | + }
46 | +
47 | t = g_hash_table_lookup (vars, "DBDIR");
48 | if (t) {
49 | dbdir = t;
50 | @@ -669,6 +680,7 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
51 | lua_createtable (L, 0, 9);
52 |
53 | rspamd_lua_table_set (L, RSPAMD_CONFDIR_INDEX, confdir);
54 | + rspamd_lua_table_set (L, RSPAMD_LOCAL_CONFDIR_INDEX, local_confdir);
55 | rspamd_lua_table_set (L, RSPAMD_RUNDIR_INDEX, rundir);
56 | rspamd_lua_table_set (L, RSPAMD_DBDIR_INDEX, dbdir);
57 | rspamd_lua_table_set (L, RSPAMD_LOGDIR_INDEX, logdir);
58 | diff --git a/src/lua/lua_common.h b/src/lua/lua_common.h
59 | index 838e0fe7a..5810184b7 100644
60 | --- a/src/lua/lua_common.h
61 | +++ b/src/lua/lua_common.h
62 | @@ -410,6 +410,7 @@ gboolean rspamd_lua_require_function (lua_State *L, const gchar *modname,
63 |
64 | /* Paths defs */
65 | #define RSPAMD_CONFDIR_INDEX "CONFDIR"
66 | +#define RSPAMD_LOCAL_CONFDIR_INDEX "LOCAL_CONFDIR"
67 | #define RSPAMD_RUNDIR_INDEX "RUNDIR"
68 | #define RSPAMD_DBDIR_INDEX "DBDIR"
69 | #define RSPAMD_LOGDIR_INDEX "LOGDIR"
70 | diff --git a/src/lua/lua_config.c b/src/lua/lua_config.c
71 | index 2093cbe01..d5d80914b 100644
72 | --- a/src/lua/lua_config.c
73 | +++ b/src/lua/lua_config.c
74 | @@ -3339,6 +3339,7 @@ lua_config_load_ucl (lua_State *L)
75 |
76 | if (lua_istable (L, -1)) {
77 | LUA_TABLE_TO_HASH(paths, RSPAMD_CONFDIR_INDEX);
78 | + LUA_TABLE_TO_HASH(paths, RSPAMD_LOCAL_CONFDIR_INDEX);
79 | LUA_TABLE_TO_HASH(paths, RSPAMD_RUNDIR_INDEX);
80 | LUA_TABLE_TO_HASH(paths, RSPAMD_DBDIR_INDEX);
81 | LUA_TABLE_TO_HASH(paths, RSPAMD_LOGDIR_INDEX);
82 |
--------------------------------------------------------------------------------
/modules/core/rspamd/rspamd-1.8.0-local-rules.patch:
--------------------------------------------------------------------------------
1 | diff --git a/rules/rspamd.lua b/rules/rspamd.lua
2 | index a193eb495..67136cc6e 100644
3 | --- a/rules/rspamd.lua
4 | +++ b/rules/rspamd.lua
5 | @@ -21,7 +21,7 @@ require "global_functions" ()
6 | config['regexp'] = {}
7 | rspamd_maps = {} -- Global maps
8 |
9 | -local local_conf = rspamd_paths['CONFDIR']
10 | +local local_conf = rspamd_paths['LOCAL_CONFDIR']
11 | local local_rules = rspamd_paths['RULESDIR']
12 | local rspamd_util = require "rspamd_util"
13 |
14 | diff --git a/src/lua/lua_common.c b/src/lua/lua_common.c
15 | index ac7a393b8..233397fc0 100644
16 | --- a/src/lua/lua_common.c
17 | +++ b/src/lua/lua_common.c
18 | @@ -581,6 +581,7 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
19 | lua_getglobal (L, "rspamd_paths");
20 | if (lua_isnil (L, -1)) {
21 | const gchar *confdir = RSPAMD_CONFDIR, *rundir = RSPAMD_RUNDIR,
22 | + *local_confdir = RSPAMD_LOCAL_CONFDIR,
23 | *dbdir = RSPAMD_DBDIR, *logdir = RSPAMD_LOGDIR,
24 | *wwwdir = RSPAMD_WWWDIR, *pluginsdir = RSPAMD_PLUGINSDIR,
25 | *rulesdir = RSPAMD_RULESDIR, *lualibdir = RSPAMD_LUALIBDIR,
26 | @@ -628,6 +629,11 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
27 | confdir = t;
28 | }
29 |
30 | + t = getenv ("LOCAL_CONFDIR");
31 | + if (t) {
32 | + local_confdir = t;
33 | + }
34 | +
35 |
36 | if (vars) {
37 | t = g_hash_table_lookup (vars, "PLUGINSDIR");
38 | @@ -660,6 +666,11 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
39 | confdir = t;
40 | }
41 |
42 | + t = g_hash_table_lookup (vars, "LOCAL_CONFDIR");
43 | + if (t) {
44 | + local_confdir = t;
45 | + }
46 | +
47 | t = g_hash_table_lookup (vars, "DBDIR");
48 | if (t) {
49 | dbdir = t;
50 | @@ -674,6 +685,7 @@ rspamd_lua_set_globals (struct rspamd_config *cfg, lua_State *L,
51 | lua_createtable (L, 0, 9);
52 |
53 | rspamd_lua_table_set (L, RSPAMD_CONFDIR_INDEX, confdir);
54 | + rspamd_lua_table_set (L, RSPAMD_LOCAL_CONFDIR_INDEX, local_confdir);
55 | rspamd_lua_table_set (L, RSPAMD_RUNDIR_INDEX, rundir);
56 | rspamd_lua_table_set (L, RSPAMD_DBDIR_INDEX, dbdir);
57 | rspamd_lua_table_set (L, RSPAMD_LOGDIR_INDEX, logdir);
58 | diff --git a/src/lua/lua_common.h b/src/lua/lua_common.h
59 | index af0b5f824..fccbf5115 100644
60 | --- a/src/lua/lua_common.h
61 | +++ b/src/lua/lua_common.h
62 | @@ -425,6 +425,7 @@ gboolean rspamd_lua_require_function (lua_State *L, const gchar *modname,
63 |
64 | /* Paths defs */
65 | #define RSPAMD_CONFDIR_INDEX "CONFDIR"
66 | +#define RSPAMD_LOCAL_CONFDIR_INDEX "LOCAL_CONFDIR"
67 | #define RSPAMD_RUNDIR_INDEX "RUNDIR"
68 | #define RSPAMD_DBDIR_INDEX "DBDIR"
69 | #define RSPAMD_LOGDIR_INDEX "LOGDIR"
70 | diff --git a/src/lua/lua_config.c b/src/lua/lua_config.c
71 | index d72c5ff66..b609348fe 100644
72 | --- a/src/lua/lua_config.c
73 | +++ b/src/lua/lua_config.c
74 | @@ -3441,6 +3441,7 @@ lua_config_load_ucl (lua_State *L)
75 |
76 | if (lua_istable (L, -1)) {
77 | LUA_TABLE_TO_HASH(paths, RSPAMD_CONFDIR_INDEX);
78 | + LUA_TABLE_TO_HASH(paths, RSPAMD_LOCAL_CONFDIR_INDEX);
79 | LUA_TABLE_TO_HASH(paths, RSPAMD_RUNDIR_INDEX);
80 | LUA_TABLE_TO_HASH(paths, RSPAMD_DBDIR_INDEX);
81 | LUA_TABLE_TO_HASH(paths, RSPAMD_LOGDIR_INDEX);
82 |
--------------------------------------------------------------------------------
/modules/core/testing.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ... }:
2 |
3 | let
4 | inherit (lib) types mkOption;
5 | in {
6 | options.nixcloud.tests = {
7 | enable = mkOption {
8 | type = types.bool;
9 | default = true;
10 | example = false;
11 | description = ''
12 | Whether to run tests needed by the current system configuration.
13 | '';
14 | };
15 |
16 | wanted = mkOption {
17 | type = let
18 | testName = types.either types.str (types.listOf types.str);
19 | pathOrName = types.either types.path testName;
20 | in types.listOf pathOrName;
21 | default = [];
22 | apply = val: let
23 | unified = map (x: if lib.isString x then lib.singleton x else x) val;
24 | in lib.unique unified;
25 | example = [ "reverse-proxy" ["foo" "bar"] ];
26 | description = ''
27 | A list of tests needed for the current configuration.
28 |
29 | The list elements can either be plain strings or a list of strings,
30 | where the latter will form an attribute path.
31 | '';
32 | };
33 | };
34 |
35 | config = lib.mkIf config.nixcloud.tests.enable {
36 | system.extraDependencies = let
37 | system = if config.nixpkgs ? localSystem then config.nixpkgs.localSystem.system else config.nixpkgs.system;
38 | testRoot = import ../../tests { inherit pkgs system; };
39 | mkErr = path: let
40 | pathStr = lib.concatStringsSep "." path;
41 | in abort "Unable to find test for path ${pathStr}.";
42 | getTest = arg: let
43 | fromAttrPath = lib.attrByPath arg (mkErr arg) testRoot;
44 | fromPath = import ../../lib/call-test.nix { inherit pkgs system; } arg;
45 | in if lib.isList arg then fromAttrPath else fromPath;
46 | in map getTest config.nixcloud.tests.wanted;
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/modules/core/utils.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ... }:
2 |
3 | {
4 | _module.args.nclib = rec {
5 | # Runs the specified function on the configuration of all defined web
6 | # service and returns the results as a list.
7 | #
8 | # Let's say the following web services are defined:
9 | #
10 | # {
11 | # nixcloud.webservices.foo1.bar1.someOption = "aaa";
12 | # nixcloud.webservices.foo1.bar2.someOption = "bbb";
13 | # nixcloud.webservices.foo2.bar1.someOption = "ccc";
14 | # }
15 | #
16 | # ... and using mapWSConfigToList like this:
17 | #
18 | # mapWSConfigToList (x: someOption)
19 | #
20 | # Will result in the list: [ "aaa" "bbb" "ccc" ]
21 | mapWSConfigToList = mapWSConfigToListCond (x: true);
22 |
23 | # Same as mapWSConfigToList but allows to specify a filter function which
24 | # is given the actual web service config and should return true for configs
25 | # to include or falso for configs to discard.
26 | mapWSConfigToListCond = cond: fun: let
27 | inherit (config.nixcloud) webservices;
28 | applyCond = cfg: lib.optional (cond cfg) (fun cfg);
29 | getConfig = lib.mapAttrsToList (lib.const applyCond);
30 | getFlatCfg = wscfg: lib.concatLists (getConfig wscfg);
31 | in lib.concatLists (lib.mapAttrsToList (lib.const getFlatCfg) webservices);
32 |
33 | # Map UNIX Domain Sockets to Unix sockets, example usage:
34 | #
35 | # ip2unix {
36 | # program = "${pkgs.nginx}/bin/nginx";
37 | # rules = [
38 | # { direction = "outgoing"; socketPath = "/run/other.sock"; }
39 | # { address = "127.0.0.1"; socketPath = "/run/local.sock"; }
40 | # { socketActivation = true; fdName = "foo" }
41 | # ];
42 | # }
43 | ip2unix = args: (lib.evalModules {
44 | modules = [
45 | { config._module.args = { inherit pkgs; }; }
46 | { config = args; }
47 | ./ip2unix.nix
48 | ];
49 | }).config.__result;
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/modules/core/version.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ... }:
2 |
3 | let
4 | gitDir = toString ../../.git;
5 | hasGitDir = lib.pathIsDirectory gitDir;
6 | commitId = lib.commitIdFromGitRepo gitDir;
7 |
8 | nixcloudVersionScript = pkgs.writeScriptBin "nixcloud-version" ''
9 | #!${pkgs.stdenv.shell}
10 | echo ${lib.escapeShellArg config.nixcloud.version}
11 | '';
12 |
13 | in {
14 | options.nixcloud.version = lib.mkOption {
15 | type = lib.types.str;
16 | default = if hasGitDir then commitId else "master";
17 | internal = true;
18 | description = "The git revision of nixcloud-webservices.";
19 | };
20 |
21 | config.environment.systemPackages = lib.singleton nixcloudVersionScript;
22 | config.system.nixos.versionSuffix = let
23 | nixpkgsRev = lib.substring 0 7 config.system.nixos.revision;
24 | abbrevVersion = lib.substring 0 7 config.nixcloud.version;
25 | in ".${nixpkgsRev}-nixcloud_${abbrevVersion}";
26 | }
27 |
--------------------------------------------------------------------------------
/modules/default.nix:
--------------------------------------------------------------------------------
1 | {
2 | disabledModules = [
3 | # https://github.com/NixOS/nixpkgs/pull/29365
4 | "services/mail/opendkim.nix"
5 | # Needed because services.postfix.relayPort is only available in 18.03
6 | "services/mail/postfix.nix"
7 | "services/mail/pfix-srsd.nix"
8 | ];
9 |
10 | imports = [
11 | core/dbshell
12 | core/directories
13 | core/packages.nix
14 | core/hashed-modules.nix
15 | core/testing.nix
16 | core/utils.nix
17 | core/version.nix
18 | services/reverse-proxy
19 | services/TLS
20 | services/email/rspamd.nix
21 | services/email/opendkim.nix
22 | services/email/postfix.nix
23 | services/email/pfix-srsd.nix
24 | services/email/nixcloud-email.nix
25 | virtualisation/container.nix
26 | ./web
27 | ];
28 | }
29 |
--------------------------------------------------------------------------------
/modules/services/TLS/common.nix:
--------------------------------------------------------------------------------
1 | { lib }:
2 |
3 | {
4 | stateDir = "/var/lib/nixcloud/TLS";
5 |
6 | # to prevent accidentally exceeding the ACME's rate limit (API) we hash the
7 | # option definitions in a way that the order of the 'inputs' as domains,
8 | # extraDomains and API endpoint don't affect the generated certificate
9 | hashACMEConfig = cfg: let
10 | server = cfg.acmeApiEndpoint;
11 | h = lib.fold (el: c: c // { ${el} = ""; }) {} ([ cfg.domain ] ++ cfg.extraDomains ++ [ server ]);
12 | in
13 | builtins.hashString "sha256" (builtins.toJSON h);
14 | }
15 |
--------------------------------------------------------------------------------
/modules/services/TLS/module1.nix:
--------------------------------------------------------------------------------
1 | { ... } : {
2 | # nixcloud.TLS.certs = {
3 | # "example.com" = {
4 | # domain = "aaaaaaaaahhhhh.bb";
5 | # #mode = "ACME";
6 | # extraDomains = [ "linux.org" ];
7 | # email = "foo@bar.com";
8 | # restart = [ "dovecot2.service" "foo.service" ];
9 | # reload = [ "foo.service" ];
10 | # };
11 | # };
12 | }
13 |
--------------------------------------------------------------------------------
/modules/services/TLS/module2.nix:
--------------------------------------------------------------------------------
1 | { ... } : {
2 |
3 | #
4 | # nixcloud.TLS.certs = {
5 | # "foo.de" = {
6 | # domain = "foo.de1";
7 | # mode = "ACME";
8 | # };
9 | # "example.com" = {
10 | # domain = "aaaaaaaaahhhhh.bb";
11 | # extraDomains = [ "flux.com" "flux.com" ];
12 | # mode = "ACME";
13 | # email = "foo@bar.com";
14 | # restart = [ "foo.service" "foo.service"];
15 | # reload = [ "foo.service" "bar" ];
16 | # };
17 | # #"foo.com".mode = {
18 | # # ssl_certificate_key = /flux/to/cert.pem;
19 | # # ssl_certificate = /flux/to/key.pem;
20 | # #};
21 | # };
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/filter_bin/rspamd.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -o errexit
3 | cat | rspamc --header="settings-id=delivery" -d "$1" -h /run/rspamd/worker-controller.sock -m
4 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/imap_sieve/report-ham.sieve:
--------------------------------------------------------------------------------
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2 |
3 | if environment :matches "imap.mailbox" "*" {
4 | set "mailbox" "${1}";
5 | }
6 |
7 | if string "${mailbox}" "Trash" {
8 | stop;
9 | }
10 |
11 | if environment :matches "imap.user" "*" {
12 | set "username" "${1}";
13 | }
14 |
15 | if environment :matches "imap.email" "*" {
16 | set "email" "${1}";
17 | }
18 |
19 | pipe :copy "learn-ham.sh" [ "${username}", "${email}" ];
20 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/imap_sieve/report-spam.sieve:
--------------------------------------------------------------------------------
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2 |
3 | if environment :matches "imap.user" "*" {
4 | set "username" "${1}";
5 | }
6 |
7 | if environment :matches "imap.email" "*" {
8 | set "email" "${1}";
9 | }
10 |
11 | pipe :copy "learn-spam.sh" [ "${username}", "${email}" ];
12 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/pipe_bin/learn-ham.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -o errexit
3 | exec rspamc -c bayes_users -d "$2" -h /run/rspamd/worker-controller.sock learn_ham
4 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/pipe_bin/learn-spam.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -o errexit
3 | exec rspamc -c bayes_users -d "$2" -h /run/rspamd/worker-controller.sock learn_spam
4 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/sieve/file-spam.sieve:
--------------------------------------------------------------------------------
1 | require ["fileinto", "reject", "envelope", "mailbox", "reject"];
2 |
3 | # spamassassin
4 | if header :contains "X-Spam-Flag" "YES" {
5 | fileinto :create "Spam";
6 | stop;
7 | }
8 | # rspamd
9 | if header :contains "X-Spam" "YES" {
10 | fileinto :create "Spam";
11 | stop;
12 | }
13 |
--------------------------------------------------------------------------------
/modules/services/email/dovecot/sieve/rspamd.sieve:
--------------------------------------------------------------------------------
1 | require ["vnd.dovecot.filter", "envelope", "variables"];
2 |
3 | if envelope :matches "to" "*" {
4 | set "destination" "${1}";
5 | }
6 |
7 | filter "rspamd.sh" [ "${destination}" ];
8 |
--------------------------------------------------------------------------------
/modules/services/email/opendkim.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, ... }:
2 |
3 | with lib;
4 |
5 | let
6 |
7 | cfg = config.services.opendkim;
8 |
9 | defaultSock = "local:/run/opendkim/opendkim.sock";
10 |
11 | keyFile = "${cfg.keyPath}/${cfg.selector}.private";
12 |
13 | args = [ "-f" "-l"
14 | "-p" cfg.socket
15 | "-d" cfg.domains
16 | "-k" keyFile
17 | "-s" cfg.selector
18 | ] ++ optionals (cfg.configFile != null) [ "-x" cfg.configFile ];
19 |
20 | in {
21 |
22 | ###### interface
23 |
24 | options = {
25 |
26 | services.opendkim = {
27 |
28 | enable = mkOption {
29 | type = types.bool;
30 | default = false;
31 | description = "Whether to enable the OpenDKIM sender authentication system.";
32 | };
33 |
34 | socket = mkOption {
35 | type = types.str;
36 | default = defaultSock;
37 | description = "Socket which is used for communication with OpenDKIM.";
38 | };
39 |
40 | user = mkOption {
41 | type = types.str;
42 | default = "opendkim";
43 | description = "User for the daemon.";
44 | };
45 |
46 | group = mkOption {
47 | type = types.str;
48 | default = "opendkim";
49 | description = "Group for the daemon.";
50 | };
51 |
52 | domains = mkOption {
53 | type = types.str;
54 | default = "csl:${config.networking.hostName}";
55 | example = "csl:example.com,mydomain.net";
56 | description = ''
57 | Local domains set (see opendkim(8) for more information on datasets).
58 | Messages from them are signed, not verified.
59 | '';
60 | };
61 |
62 | keyPath = mkOption {
63 | type = types.path;
64 | description = ''
65 | The path that opendkim should put its generated private keys into.
66 | The DNS settings will be found in this directory with the name selector.txt.
67 | '';
68 | default = "/var/lib/opendkim/keys";
69 | };
70 |
71 | selector = mkOption {
72 | type = types.str;
73 | description = "Selector to use when signing.";
74 | };
75 |
76 | configFile = mkOption {
77 | type = types.nullOr types.path;
78 | default = null;
79 | description = "Additional opendkim configuration.";
80 | };
81 |
82 | };
83 |
84 | };
85 |
86 |
87 | ###### implementation
88 |
89 | config = mkIf cfg.enable {
90 |
91 | users.extraUsers = optionalAttrs (cfg.user == "opendkim") { opendkim =
92 | { group = cfg.group;
93 | uid = config.ids.uids.opendkim;
94 | };};
95 |
96 | users.extraGroups = optionalAttrs (cfg.group == "opendkim") { opendkim =
97 | { gid = config.ids.gids.opendkim;
98 | };};
99 |
100 | environment.systemPackages = [ pkgs.opendkim ];
101 |
102 | systemd.services.opendkim = {
103 | description = "OpenDKIM signing and verification daemon";
104 | after = [ "network.target" ];
105 | wantedBy = [ "multi-user.target" ];
106 |
107 | preStart = ''
108 | mkdir -p "${cfg.keyPath}"
109 | cd "${cfg.keyPath}"
110 | if ! test -f ${cfg.selector}.private; then
111 | ${pkgs.opendkim}/bin/opendkim-genkey -s ${cfg.selector} -d all-domains-generic-key
112 | echo "Generated OpenDKIM key! Please update your DNS settings:\n"
113 | echo "-------------------------------------------------------------"
114 | cat ${cfg.selector}.txt
115 | echo "-------------------------------------------------------------"
116 | fi
117 | chown ${cfg.user}:${cfg.group} ${cfg.selector}.private
118 | '';
119 |
120 | serviceConfig = {
121 | ExecStart = "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
122 | User = cfg.user;
123 | Group = cfg.group;
124 | RuntimeDirectory = optional (cfg.socket == defaultSock) "opendkim";
125 | PermissionsStartOnly = true;
126 | };
127 | };
128 |
129 | };
130 | }
131 |
--------------------------------------------------------------------------------
/modules/services/email/pfix-srsd.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, ... }:
2 |
3 | with lib;
4 |
5 | {
6 |
7 | ###### interface
8 |
9 | options = {
10 |
11 | services.pfix-srsd = {
12 | enable = mkOption {
13 | default = false;
14 | type = types.bool;
15 | description = "Whether to run the postfix sender rewriting scheme daemon.";
16 | };
17 |
18 | domain = mkOption {
19 | description = "The domain for which to enable srs";
20 | type = types.str;
21 | example = "example.com";
22 | };
23 |
24 | secretsFile = mkOption {
25 | description = ''
26 | The secret data used to encode the SRS address.
27 | to generate, use a command like:
28 | for n in $(seq 5); do dd if=/dev/urandom count=1 bs=1024 status=none | sha256sum | sed 's/ -$//' | sed 's/^/ /'; done
29 | '';
30 | type = types.path;
31 | default = "/var/lib/pfix-srsd/secrets";
32 | };
33 | };
34 | };
35 |
36 | ###### implementation
37 |
38 | config = mkIf config.services.pfix-srsd.enable {
39 | environment = {
40 | systemPackages = [ pkgs.pfixtools ];
41 | };
42 |
43 | systemd.services."pfix-srsd" = {
44 | description = "Postfix sender rewriting scheme daemon";
45 | before = [ "postfix.service" ];
46 | #note that we use requires rather than wants because postfix
47 | #is unable to process (almost) all mail without srsd
48 | requiredBy = [ "postfix.service" ];
49 | serviceConfig = {
50 | Type = "forking";
51 | PIDFile = "/var/run/pfix-srsd.pid";
52 | ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /var/run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}";
53 | };
54 | };
55 | };
56 | }
--------------------------------------------------------------------------------
/modules/services/email/rspamd.nix:
--------------------------------------------------------------------------------
1 | # Replacement from master branch 24. Nov. 2018
2 | # This includes:
3 | # https://github.com/NixOS/nixpkgs/pull/51012
4 | # https://github.com/NixOS/nixpkgs/pull/49809
5 | # https://github.com/NixOS/nixpkgs/pull/49792
6 | # https://github.com/NixOS/nixpkgs/pull/49620
7 | # Can be removed after 19.03 release
8 | {pkgs, ...}:
9 | let
10 | nixpkgs = builtins.fetchTarball {
11 | url = https://github.com/NixOS/nixpkgs/archive/0d753af6617bb74535af0601a2cdce1a8c647889.tar.gz;
12 | sha256 = "1jsvf4akcwifjmgyhnc5s7zl7wss93vpm23z5yxyzy7vnh9x429c";
13 | };
14 | in {
15 | disabledModules = [
16 | "services/mail/rspamd.nix"
17 | "services/mail/rspamd.nix:anon-1"
18 | "services/mail/rspamd.nix:anon-2"
19 | "services/mail/rspamd.nix:anon-3"
20 | ];
21 | imports = [
22 | "${nixpkgs}/nixos/modules/services/mail/rspamd.nix"
23 | ];
24 | }
25 |
--------------------------------------------------------------------------------
/modules/services/email/rspamd/groups.conf:
--------------------------------------------------------------------------------
1 | group "bayes_user" {
2 | symbol {
3 | BAYES_SPAM_USER {
4 | weight = 4.0;
5 | description = "Message probably spam, probability: ";
6 | }
7 | }
8 | symbol {
9 | BAYES_HAM_USER {
10 | weight = -3.0;
11 | description = "Message probably ham, probability: ";
12 | }
13 | }
14 | }
15 |
16 | group "upstream" {
17 | symbol {
18 | UPSTREAM_SCORE = {
19 | weight = 1.0;
20 | description = "Loaded upstream score";
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/modules/services/email/rspamd/milter_headers.conf:
--------------------------------------------------------------------------------
1 | skip_local = false;
2 | use = ["x-spamd-result", "x-spam-status"];
3 |
--------------------------------------------------------------------------------
/modules/services/email/rspamd/rspamd.local.lua:
--------------------------------------------------------------------------------
1 | local rspamd_logger = require "rspamd_logger"
2 |
3 | rspamd_config.UPSTREAM_SCORE = {
4 | callback = function(task)
5 | local xss = task:get_header('X-Spam-Status')
6 | if xss then
7 | rspamd_logger.debugx(rspamd_config, 'Got upstream header %s', xss)
8 | local score = string.gsub(xss, "^.*%sscore=(%-?%d+%.%d+)$", "%1")
9 | rspamd_logger.debugx(rspamd_config, 'Got upstream score %s', score)
10 | return tonumber(score)
11 | else
12 | rspamd_logger.debugx(rspamd_config, 'Found no upstream header')
13 | return 0.0
14 | end
15 | end,
16 | score = 1,
17 | description = 'Score from milter scan',
18 | group = "upstream",
19 | }
20 | rspamd_logger.debugx(rspamd_config, 'Work dammit!!!')
21 |
--------------------------------------------------------------------------------
/modules/services/email/rspamd/settings.conf:
--------------------------------------------------------------------------------
1 | milter {
2 | id = "milter";
3 | apply {
4 | groups_disabled = ["bayes_user", "upstream"];
5 | }
6 | }
7 | delivery {
8 | id = "delivery";
9 | apply {
10 | groups_enabled = ["bayes_user", "upstream"];
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/modules/services/email/rspamd/statistic.conf:
--------------------------------------------------------------------------------
1 | classifier "bayes" {
2 | tokenizer {
3 | name = "osb";
4 | }
5 | cache {
6 | path = "${DBDIR}/learn_cache.sqlite";
7 | }
8 | name = "bayes";
9 | min_tokens = 11;
10 | backend = "sqlite3";
11 | languages_enabled = true;
12 | min_learns = 200;
13 |
14 | statfile {
15 | symbol = "BAYES_HAM";
16 | path = "${DBDIR}/bayes.ham.sqlite";
17 | spam = false;
18 | }
19 | statfile {
20 | symbol = "BAYES_SPAM";
21 | path = "${DBDIR}/bayes.spam.sqlite";
22 | spam = true;
23 | }
24 | learn_condition =<= 0.95
34 | else
35 | cl = 'ham'
36 | in_class = prob <= 0.05
37 | end
38 |
39 | if in_class then
40 | return false,string.format('already in class %s; probability %.2f%%',
41 | cl, math.abs((prob - 0.5) * 200.0))
42 | end
43 | end
44 |
45 | return true
46 | end
47 | EOD
48 |
49 | .include(try=true; priority=1) "$LOCAL_CONFDIR/local.d/classifier-bayes.conf"
50 | .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/classifier-bayes.conf"
51 | }
52 | classifier "bayes" {
53 | tokenizer {
54 | name = "osb";
55 | }
56 | cache {
57 | path = "${DBDIR}/learn_cache.sqlite";
58 | }
59 | name = "bayes_users";
60 | min_tokens = 11;
61 | backend = "sqlite3";
62 | users_enabled = true;
63 | languages_enabled = true;
64 | min_learns = 200;
65 |
66 | statfile {
67 | symbol = "BAYES_HAM_USER";
68 | path = "${DBDIR}/bayes.ham.sqlite";
69 | spam = false;
70 | }
71 | statfile {
72 | symbol = "BAYES_SPAM_USER";
73 | path = "${DBDIR}/bayes.spam.sqlite";
74 | spam = true;
75 | }
76 | learn_condition =<= 0.95
86 | else
87 | cl = 'ham'
88 | in_class = prob <= 0.05
89 | end
90 |
91 | if in_class then
92 | return false,string.format('already in class %s; probability %.2f%%',
93 | cl, math.abs((prob - 0.5) * 200.0))
94 | end
95 | end
96 |
97 | return true
98 | end
99 | EOD
100 |
101 | .include(try=true; priority=1) "$LOCAL_CONFDIR/local.d/classifier-bayes-user.conf"
102 | .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/classifier-bayes-user.conf"
103 | }
104 |
--------------------------------------------------------------------------------
/modules/services/email/test/accounts.nix:
--------------------------------------------------------------------------------
1 | {
2 | alice = {
3 | domain = "example.org";
4 | server = "mx.example.org";
5 | plainPasswd = "testpw1";
6 | password = "{SSHA256}LDxblDvyoK+wWl1sX+TWntbfkvQ+jATEadp0Q1yfqlNLo2SZ";
7 | aliases = [ "anotheralice@example.net" ];
8 | };
9 | bob = {
10 | domain = "example.net";
11 | server = "mx.example.org";
12 | plainPasswd = "testpw2";
13 | password = "{SHA256}8NSRIfBpISneREB06+Z/4h9io5c9+9zEEe1abG6gFCQ=";
14 | };
15 | foo = {
16 | domain = "example.com";
17 | server = "mx.example.com";
18 | plainPasswd = "testpw3";
19 | password = "{PBKDF2}$1$NbF7gIsr3MBE6Ice$5000$07072413c01782d9eb2e5d"
20 | + "919f2c402b8e924c2e";
21 | };
22 | # NOTE: This account has a quota set, so be sure to *never* send any email to
23 | # it in tests other than the quota test.
24 | bar = {
25 | domain = "example.com";
26 | server = "mx.example.com";
27 | plainPasswd = "testpw4";
28 | password = "{PLAIN}testpw4";
29 | quota = "10k";
30 | };
31 | spameater = {
32 | domain = "catchall.example";
33 | server = "mx.example.com";
34 | plainPasswd = "testpw5";
35 | password = "{PLAIN}testpw5";
36 | catchallFor = [ "catchall.example" ];
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/modules/services/email/test/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs, ... }:
2 |
3 | let
4 | testAccounts = let
5 | inherit (pkgs) lib;
6 | in lib.mapAttrs (name: attrs: attrs // {
7 | address = "${name}@${attrs.domain}";
8 | }) (import ./accounts.nix);
9 |
10 | # These are the additional attributes we support in our test accounts, which
11 | # we need to remove later when generating options for the virtual users.
12 | extraTestAccountAttrs = [ "plainPasswd" "server" "address" ];
13 |
14 | mkNetworkConfig = suffix: { config, lib, ... }: {
15 | options._test-support = let
16 | mkSupportOption = default: lib.mkOption {
17 | type = lib.types.str;
18 | inherit default;
19 | internal = true;
20 | description = "Test support option";
21 | };
22 | in {
23 | v4addr = mkSupportOption "192.168.0.${toString suffix}";
24 | v4rev = mkSupportOption "${toString suffix}.0.168.192.in-addr.arpa";
25 | v6addr = mkSupportOption "abcd::${toString suffix}";
26 | v6rev = let
27 | base = lib.genList (lib.const "0") 28
28 | ++ [ "d" "c" "b" "a" "ip6" "arpa" ];
29 | singles = lib.reverseList (lib.stringToCharacters (toString suffix));
30 | result = singles ++ lib.drop (lib.length singles) base;
31 | in mkSupportOption (lib.concatStringsSep "." result);
32 | };
33 |
34 | config.networking.useDHCP = false;
35 | config.networking.interfaces.eth1 = {
36 | ipv4.addresses = lib.singleton {
37 | address = config._test-support.v4addr;
38 | prefixLength = 24;
39 | };
40 | ipv6.addresses = lib.singleton {
41 | address = config._test-support.v6addr;
42 | prefixLength = 24;
43 | };
44 | };
45 | };
46 |
47 | commonConfig = { lib, nodes, ... }: {
48 | networking.nameservers = lib.mkForce [
49 | nodes.dns.config._test-support.v4addr
50 | nodes.dns.config._test-support.v6addr
51 | ];
52 | };
53 |
54 |
55 | mkMailConfig = hostname: domains: { config, lib, ... }: {
56 | imports = [ commonConfig ];
57 | nixpkgs.overlays = lib.singleton (self: super: {
58 | # Make sure sa-update doesn't try to access the network:
59 | spamassassin = super.spamassassin.overrideAttrs (drv: {
60 | testRules = self.writeText "spamd-test-rules.cf" ''
61 | body NIXCLOUD_TEST_RULE /I am a spammer/
62 | score NIXCLOUD_TEST_RULE 20.0
63 | describe NIXCLOUD_TEST_RULE Rule for VM test
64 | '';
65 |
66 | # Our own version of sa-update which places our testRules from above so
67 | # that we can use it later to check whether spamd correctly identifies
68 | # stuff as spam.
69 | preFixup = (drv.preFixup or "") + ''
70 | make version.env
71 | source version.env
72 | rulesDir="/var/lib/spamassassin/$CPAN_VERSION"
73 | cat > "$out/bin/sa-update" < "$rulesDir/updates_spamassassin_org.cf"
78 | EOF
79 | '';
80 | });
81 | });
82 | nixcloud.email = {
83 | enable = true;
84 | enableTLS = false;
85 | ipAddress = config._test-support.v4addr;
86 | ip6Address = config._test-support.v6addr;
87 | fqdn = hostname;
88 | inherit domains;
89 | # FIXME: Disabled for now because we really don't want to wait >300
90 | # seconds for the test to run.
91 | enableGreylisting = false;
92 | webmail.enable = true;
93 | users = let
94 | isLocalUser = lib.const (attrs: lib.elem attrs.domain domains);
95 | localUsers = lib.filterAttrs isLocalUser testAccounts;
96 | in lib.mapAttrsToList (name: attrs: {
97 | inherit name;
98 | } // removeAttrs attrs extraTestAccountAttrs) localUsers;
99 | };
100 | };
101 |
102 | in {
103 | name = "email";
104 |
105 | nodes.dns = { config, pkgs, lib, nodes, ... }: {
106 | imports = lib.singleton (mkNetworkConfig 1);
107 | networking.firewall.enable = false;
108 | services.bind.enable = true;
109 | services.bind.cacheNetworks = lib.mkForce [ "any" ];
110 |
111 | # XXX/FIXME: Config file injection because the values aren't escaped in the
112 | # BIND module of . Let's actually directly fix this in
113 | # by adding an option which allows us to set options
114 | # and using something like "options { empty-zones-enable no; };"
115 | # in extraConfig won't work because BIND doesn't allow
116 | # redefinition of options.
117 | services.bind.forwarders = lib.mkForce [ "}; empty-zones-enable no; #" ];
118 |
119 | services.bind.zones = lib.singleton {
120 | name = ".";
121 | file = pkgs.writeText "root.zone" ''
122 | $TTL 3600
123 | . IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d )
124 | . IN NS ns.fakedns.
125 |
126 | ns.fakedns. IN A ${config._test-support.v4addr}
127 | ns.fakedns. IN AAAA ${config._test-support.v6addr}
128 |
129 | mail.example.org. IN A ${nodes.mail1.config._test-support.v4addr}
130 | mail.example.org. IN AAAA ${nodes.mail1.config._test-support.v6addr}
131 |
132 | mail.example.com. IN A ${nodes.mail2.config._test-support.v4addr}
133 | mail.example.com. IN AAAA ${nodes.mail2.config._test-support.v6addr}
134 |
135 | mx.example.org. IN A ${nodes.mail1.config._test-support.v4addr}
136 | mx.example.org. IN AAAA ${nodes.mail1.config._test-support.v6addr}
137 |
138 | mx.example.com. IN A ${nodes.mail2.config._test-support.v4addr}
139 | mx.example.com. IN AAAA ${nodes.mail2.config._test-support.v6addr}
140 |
141 | example.org. IN MX 10 mx.example.org.
142 | example.net. IN MX 10 mx.example.org.
143 | example.com. IN MX 10 mx.example.com.
144 | catchall.example. IN MX 10 mx.example.com.
145 |
146 | ; Reverse PTRs, note that the client doesn't have one and also
147 | ; shouldn't have one to make sure the MSA picks it up even if it's a
148 | ; dialup connection somewhere in Siberia.
149 | ${config._test-support.v4rev}. IN PTR ns.fakedns.
150 | ${config._test-support.v6rev}. IN PTR ns.fakedns.
151 |
152 | ${nodes.mail1.config._test-support.v4rev}. IN PTR mx.example.org.
153 | ${nodes.mail1.config._test-support.v6rev}. IN PTR mx.example.org.
154 |
155 | ${nodes.mail2.config._test-support.v4rev}. IN PTR mx.example.com.
156 | ${nodes.mail2.config._test-support.v6rev}. IN PTR mx.example.com.
157 | '';
158 | };
159 | };
160 |
161 | nodes.mail1.imports = [
162 | (mkMailConfig "mx.example.org" [ "example.org" "example.net" ])
163 | (mkNetworkConfig 11)
164 | ];
165 |
166 | nodes.mail2.imports = [
167 | (mkMailConfig "mx.example.com" [ "example.com" "catchall.example" ])
168 | (mkNetworkConfig 12)
169 | ];
170 |
171 | nodes.client = { lib, pkgs, ... }: {
172 | imports = [ commonConfig (mkNetworkConfig 100) ];
173 | environment.systemPackages = lib.singleton (pkgs.runCommand "run-tests" {
174 | src = ./test.py;
175 | testAccounts = builtins.toJSON testAccounts;
176 | nativeBuildInputs = lib.singleton pkgs.makeWrapper;
177 | inherit (pkgs.python3) interpreter;
178 | } ''
179 | mkdir -p "$out/bin"
180 | makeWrapper "$interpreter" "$out/bin/run-tests" \
181 | --set TEST_ACCOUNTS "$testAccounts" \
182 | --add-flags "$src"
183 | '');
184 | };
185 |
186 | testScript = ''
187 | startAll;
188 | $dns->waitForUnit('bind.service');
189 | $mail1->waitForUnit('multi-user.target');
190 | $mail2->waitForUnit('multi-user.target');
191 | $client->waitForUnit('multi-user.target');
192 | $client->succeed('run-tests >&2');
193 | # wait for reverse-proxy
194 | $mail1->waitForOpenPort(80);
195 | $mail2->waitForOpenPort(80);
196 | $mail1->waitForOpenPort(8993);
197 | $mail2->waitForOpenPort(8993);
198 | $mail1->succeed('curl -L http://mail.example.org/ | grep -qF "Roundcube"');
199 | # Check spam learning
200 | $mail2->waitUntilSucceeds("journalctl -u dovecot2 | grep learn-spam.sh >&2");
201 | $mail2->succeed('journalctl -u rspamd | grep "csession; rspamd_controller_learn_fin_task: learned message as spam" >&2');
202 | $mail2->waitUntilSucceeds("journalctl -u dovecot2 | grep learn-ham.sh >&2");
203 | $mail2->succeed('journalctl -u rspamd | grep "csession; rspamd_controller_learn_fin_task: learned message as ham" >&2');
204 | '';
205 | }
206 |
--------------------------------------------------------------------------------
/modules/services/email/test/test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import imaplib
3 | import smtplib
4 | import json
5 | import os
6 | import re
7 |
8 | from email.mime.text import MIMEText
9 |
10 | ACCOUNTS = json.loads(os.getenv('TEST_ACCOUNTS', '{}'))
11 |
12 | RE_DSN_FAILED = re.compile(r'could.+not.+be.+delivered', re.DOTALL)
13 | RE_IS_SPAM = re.compile(r'original *message *before *SpamAssassin', re.DOTALL)
14 |
15 |
16 | class EmailTest(unittest.TestCase):
17 | def setUp(self):
18 | self.accounts = ACCOUNTS.copy()
19 | for attrs in self.accounts.values():
20 | attrs['imap'] = imaplib.IMAP4(attrs['server'])
21 | attrs['imap'].login(attrs['address'], attrs['plainPasswd'])
22 |
23 | def tearDown(self):
24 | for attrs in self.accounts.values():
25 | attrs['imap'].logout()
26 |
27 | def send_email(self, sender, recipient, body, to_addr=None, subject=None):
28 | from_attrs = self.accounts[sender]
29 | from_addr = from_attrs['address']
30 |
31 | if to_addr is None:
32 | to_addr = self.accounts[recipient]['address']
33 |
34 | msg = MIMEText(body)
35 | msg['Subject'] = 'Test mail' if subject is None else subject
36 | msg['From'] = from_addr
37 | msg['To'] = to_addr
38 |
39 | with smtplib.SMTP(from_attrs['server'], port=587) as smtp:
40 | smtp.login(from_addr, from_attrs['plainPasswd'])
41 | smtp.sendmail(from_addr, [to_addr], msg.as_string())
42 |
43 | def wait_for_new_emails(self, user=None, folder='INBOX', search='(UNSEEN)'):
44 | newmails = []
45 | while len(newmails) == 0:
46 | for name, attrs in self.accounts.items():
47 | if user is not None and name != user:
48 | continue
49 | try:
50 | attrs['imap'].select(folder)
51 | except imaplib.IMAP4.error:
52 | continue
53 | response = attrs['imap'].search(None, search)[1]
54 | unread_msg_ids = response[0].split()
55 | for msgid in unread_msg_ids:
56 | result = attrs['imap'].fetch(msgid, '(UID BODY[TEXT])')[1]
57 | newmails.append((msgid.strip().decode(), result[0][1].strip().decode()))
58 | attrs['imap'].close()
59 | return newmails
60 |
61 | def wait_for_one_email(self, user=None, folder='INBOX', search='(UNSEEN)'):
62 | newmails = self.wait_for_new_emails(user, folder, search)
63 | self.assertEqual(len(newmails), 1)
64 | return newmails[0]
65 |
66 | def mark_as_spam(self, user, msg_ids):
67 | for name, attrs in self.accounts.items():
68 | if name != user:
69 | continue
70 | attrs['imap'].select()
71 | attrs['imap'].copy(','.join(msg_ids), 'Spam')
72 | for num in msg_ids:
73 | attrs['imap'].store(num, '+FLAGS', '\\Deleted')
74 | attrs['imap'].expunge()
75 | attrs['imap'].close()
76 |
77 | def mark_as_ham(self, user, msg_ids):
78 | for name, attrs in self.accounts.items():
79 | if name != user:
80 | continue
81 | attrs['imap'].select('Spam')
82 | attrs['imap'].copy(','.join(msg_ids), 'INBOX')
83 | for num in msg_ids:
84 | attrs['imap'].store(num, '+FLAGS', '\\Deleted')
85 | attrs['imap'].expunge()
86 | attrs['imap'].close()
87 |
88 | def test_send_to_same_server(self):
89 | print ("test_send_to_same_server ~~~~")
90 | self.send_email('alice', 'bob', 'Hello Bob from Alice!')
91 | (_, text) = self.wait_for_one_email('bob')
92 | self.assertEqual(text, 'Hello Bob from Alice!')
93 |
94 | def test_send_to_different_server(self):
95 | print ("test_send_to_different_server ~~~~")
96 | self.send_email('foo', 'bob', 'Hello Bob from Foo!')
97 | (_, text) = self.wait_for_one_email('bob')
98 | self.assertEqual(text, 'Hello Bob from Foo!')
99 |
100 | def test_send_to_catchall(self):
101 | print ("test_send_to_catchall ~~~~")
102 | self.send_email('bar', 'spameater', 'Eat this!',
103 | to_addr='spam@catchall.example')
104 | (_, text) = self.wait_for_one_email('spameater')
105 | self.assertEqual(text, 'Eat this!')
106 |
107 | def test_check_quota(self):
108 | print ("test_check_quota ~~~~")
109 | msg = ("Hello, how's your quota? " * 10).strip()
110 | self.send_email('alice', 'bar', msg)
111 | (_, text) = self.wait_for_one_email('bar')
112 | self.assertEqual(text, msg)
113 |
114 | msg = ("Where is your quota? " * 1000).strip()
115 | self.send_email('bob', 'bar', msg)
116 | (_, text) = self.wait_for_one_email('bob')
117 | self.assertRegex(text, RE_DSN_FAILED)
118 |
119 | def test_aliases(self):
120 | print ("test_aliases ~~~~")
121 | msg = 'Hi different Alice!'
122 | self.send_email('spameater', 'alice', msg,
123 | to_addr='anotheralice@example.net')
124 | (_, text) = self.wait_for_one_email('alice')
125 | self.assertEqual(text, msg)
126 |
127 | def test_softbounce_nonexisting_address(self):
128 | print ("test_softbounce_nonexisting_address ~~~~")
129 | msg = 'Is there anyone?'
130 | self.send_email('alice', None, msg, to_addr='xxx@example.com')
131 | (_, text) = self.wait_for_one_email('alice')
132 | self.assertRegex(text, RE_DSN_FAILED)
133 | self.assertIn("This is the mail system at host mx.example.com",
134 | text)
135 |
136 | def test_spam_filter(self):
137 | print ("test_spam_filter ~~~~")
138 | with self.assertRaises(smtplib.SMTPDataError) as cm:
139 | msg = 'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X'
140 | self.send_email('bob', 'spameater', msg,
141 | to_addr='eatit@catchall.example')
142 | err = cm.exception
143 | self.assertEqual(554, err.smtp_code)
144 | self.assertEqual(b'5.7.1 Gtube pattern', err.smtp_error)
145 |
146 | def test_spam_filter_add_header(self):
147 | print ("test_spam_filter_add_header ~~~~")
148 | msg = 'YJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X'
149 | self.send_email('spameater', 'alice', msg,
150 | to_addr='anotheralice@example.net')
151 | (_, text) = self.wait_for_one_email('alice', 'Spam')
152 | self.assertEqual(text, msg)
153 |
154 | def test_spam_filter_train(self):
155 | print ("test_spam_filter_train ~~~~")
156 | msg = 'This is a message to train the stats filter and see how it works.'
157 | self.send_email('spameater', 'foo', msg,
158 | to_addr='foo@example.com')
159 | (msgid, text) = self.wait_for_one_email('foo')
160 | self.assertEqual(text, msg)
161 | self.mark_as_spam('foo', [msgid])
162 | (msgid, text) = self.wait_for_one_email('foo', 'Spam', 'ALL')
163 | self.assertEqual(text, msg)
164 | self.mark_as_ham('foo', [msgid])
165 |
166 | if __name__ == '__main__':
167 | unittest.main(verbosity=2)
168 |
--------------------------------------------------------------------------------
/modules/services/email/virtual-mail-submodule.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ... }:
2 | with lib;
3 | {
4 | options = {
5 | name = mkOption {
6 | type = types.str;
7 | example = "postmaster";
8 | description = "'name' is the part before the @ in the email address";
9 | };
10 |
11 | password = mkOption {
12 | type = types.str;
13 | example = "{SHA256-CRYPT}$5$/CHK3ckfRJloONnq$X/16jK2NPTiZpBZZ1XVpHhPXyxPy1p0QtUNeUFrYav5";
14 | description = ''
15 | You can generate passwords like this:
16 |
17 |
18 | # doveadm pw -s sha256-crypt
19 | Enter new password:
20 | Retype new password:
21 | {SHA256-CRYPT}$5$/CHK3ckfRJloONnq$X/16jK2NPTiZpBZZ1XVpHhPXyxPy1p0QtUNeUFrYav5
22 |
23 | '';
24 | };
25 |
26 | domain = mkOption {
27 | type = types.str;
28 | example = "nixcloud.io";
29 | description = "The domain of your mailservice you want to operate dovecot2 on";
30 | };
31 |
32 | aliases = mkOption {
33 | type = types.listOf types.str;
34 | example = [ "myalias@mydomain.tld" "auchich@mydomain.tld" ];
35 | description = "A list of email addresses which are all aliases to the virtual mail user in 'name'";
36 | default = [];
37 | };
38 |
39 | quota = mkOption {
40 | type = types.str;
41 | default = "";
42 | example = "10G";
43 | description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
44 | };
45 |
46 | catchallFor = mkOption {
47 | type = types.listOf types.str;
48 | default = [];
49 | example = [ "catchall.mymail.com" "example.mail" ];
50 | description = "All domains that are catchall domains for which this user receives all emails.";
51 | };
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/modules/services/email/virtual-mail-users.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs,
2 | virtualMailDir ? "/var/lib/virtualMail", ... }:
3 |
4 | with lib;
5 | let
6 | userOptions = import ./virtual-mail-submodule.nix;
7 | virtualMailEnv = pkgs.buildEnv {
8 | name = "virtualMail-env";
9 | paths = [];
10 | postBuild = let
11 | quotaRule = q: if q == "" then "" else "userdb_quota_rule=*";
12 | bytes = q: if q == "" then "" else "bytes=${q}";
13 | passwdLine = { name, domain, password, quota, ... }: { domain = "${domain}"; line = name + ":" + password + "::::::${quotaRule quota}:${bytes quota}"; };
14 | lines = map passwdLine config.services.mailUsers.users;
15 | domains = catAttrs "domain" lines;
16 | values = domain: catAttrs "line" (filter (x: x.domain == domain) lines);
17 | fileContent = domain: concatStringsSep "\n" (values domain);
18 | lnDomainFile = domain: "ln -sf ${pkgs.writeText domain (fileContent domain)} $out/${domain}";
19 | in concatStringsSep " && " (map lnDomainFile domains);
20 | };
21 |
22 | in {
23 | options.services.mailUsers = {
24 | users = mkOption {
25 | type = types.listOf (types.submodule userOptions);
26 | example = [ { name = "js"; domain = "nixcloud.io"; password="qwertz"; } ];
27 | default = [];
28 | description = "A list of virtual mail users for which the password is managed via this abstraction";
29 | };
30 | virtualMailEnv = mkOption {
31 | default = virtualMailEnv;
32 | description = "Passwords are stored in the nix store and this virtual environment is a directory with those";
33 | };
34 | extraAliases = mkOption {
35 | type = types.lines;
36 | default = "";
37 | example = "foo@bar.tld bar@foo.tld";
38 | description = "Extra lines for the virtual aliases file.";
39 | };
40 | };
41 |
42 | config = {
43 | # FIXME: Do extra aliases before catchall and not after catchall
44 | services.postfix.virtual = concatStringsSep "\n" (flatten (map (x: map (alias: alias + " " + x.name + "@" + x.domain) x.aliases) config.services.mailUsers.users
45 | ++ map (user: map (d: "@${d} ${user.name}@${user.domain}") user.catchallFor) config.services.mailUsers.users))
46 | + "\n\n# Extra Aliases\n" + config.services.mailUsers.extraAliases;
47 | users.groups.virtualMail = { members = [ "dovecot2" ];};
48 | users.users.virtualMail = {
49 | home = virtualMailDir;
50 | createHome = true;
51 | };
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/modules/services/reverse-proxy/test/config/basicauth.nix:
--------------------------------------------------------------------------------
1 | [ {
2 | domain = "example.com";
3 | http = {
4 | mode = "on";
5 | basicAuth."foo" = "bar1";
6 | };
7 | https = {
8 | mode = "on";
9 | basicAuth."foo" = "bar2";
10 | };
11 | ip = "127.0.0.1";
12 | path = "/basicauth";
13 | port = 60000;
14 | }
15 | ]
16 |
--------------------------------------------------------------------------------
/modules/services/reverse-proxy/test/config/blog.nix:
--------------------------------------------------------------------------------
1 | [ {
2 | #_module = {
3 | # args = { name = "proxyOptions"; }; check = true;
4 | #};
5 | domain = "example.com";
6 | http = { mode = "off"; };
7 | https = { mode = "off"; };
8 | ip = "127.0.0.1";
9 | path = "/blog";
10 | port = 60001;
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/modules/services/reverse-proxy/test/config/flubb.nix:
--------------------------------------------------------------------------------
1 | [ {
2 | #_module = {
3 | # args = { name = "proxyOptions"; }; check = true;
4 | #};
5 | domain = "flubb.com";
6 | # http = { mode = ""; };
7 | https = { mode = "on"; };
8 | ip = "127.0.0.1";
9 | path = "/blog";
10 | port = 60000;
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/modules/services/reverse-proxy/test/config/websocket.nix:
--------------------------------------------------------------------------------
1 | [ {
2 | domain = "example.ws";
3 | http = { mode = "on"; };
4 | https = { mode = "on"; };
5 | websockets = {
6 | ws = {
7 | subpath = "/ws";
8 | #https.basicAuth."nixclouduser" = "password_world_readable_in_nix_store";
9 | http.mode = "on";
10 | };
11 | };
12 | ip = "127.0.0.1";
13 | path = "/myapp";
14 | port = 8080;
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/modules/services/reverse-proxy/test/config/wiki.nix:
--------------------------------------------------------------------------------
1 | [ {
2 | #_module = {
3 | # args = { name = "proxyOptions"; }; check = true;
4 | #};
5 | domain = "example.com";
6 | http = { mode = "on"; };
7 | https = { mode = "on"; };
8 | ip = "127.0.0.1";
9 | path = "/wiki";
10 | port = 60000;
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/modules/services/reverse-proxy/test/default.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ... }:
2 | with lib;
3 |
4 | {
5 | # import config files (nixcloud.io specific for reverse proxy configuration)
6 | # use the nix module system to have type validation and inherit meaningful
7 | # default values for options which are not set explicitly
8 | config = {
9 | };
10 |
11 | imports =
12 | let
13 | cDir = builtins.toPath (./config);
14 |
15 | filesToLoad = attrNames (filterAttrs (k: v: v == "regular") (builtins.readDir cDir));
16 | configsFromPath = map (el: (cDir + ("/" + el) )) filesToLoad;
17 | toModule = x: ({ config, pkgs, lib, ... }: {
18 | options = {};
19 | config.nixcloud.reverse-proxy.extraMappings = x;
20 | });
21 | in
22 | fold (el: c: c ++ [(toModule (import el))]) [ ] configsFromPath;
23 | }
24 |
--------------------------------------------------------------------------------
/modules/virtualisation/container.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, ...}:
2 |
3 | with lib;
4 |
5 | {
6 | options = {
7 | nixcloud = {
8 | container = {
9 | enable = mkOption {
10 | type = types.bool;
11 | default = false;
12 | example = true;
13 | description = "Enable nixcloud LXC based container support";
14 | };
15 | internetInterface = mkOption {
16 | type = types.str;
17 | default = "enp0s3";
18 | example = "enp4s0";
19 | description = ''
20 | The network interface used by LXC to access the internet:
21 |
22 | * used for masquerading with the brNC-internet interface so containers can access the internet.
23 | * used for IPv6 internet access (if configured in nixcloud.container.ipv6)
24 |
25 | See also: https://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames
26 | '';
27 | };
28 | ipv6 = mkOption {
29 | description = ''
30 | If IPv6 support within the LXC based containers is wanted, provide all the required options.
31 |
32 | With these options a DHCPD6 server is dynamically/statefully serving IPV6 addresses to guests so that
33 | these can access the internet using a routable IPv6 address or can connected from outside.
34 |
35 | This provides full dynamic IPv6 support for our monitoring backend which uses `nixcloud.container`.
36 |
37 | NOTE: The IPv6 addresses are not permanent, persistent or static! Support for such setups will be implemented
38 | but after the initial release of `nixcloud.container`.
39 | '';
40 | example = {
41 | enable = true;
42 | ipv6InternetInterfaceAddress = "2001:0db8::1";
43 | ipv6Prefix = "2001:0db8::";
44 | ipv6PrefixLength = 32;
45 | ipv6NameServers = [ "2001:0db8::55" "2001:0db8::66" ];
46 | };
47 | default = {};
48 | type = types.submodule {
49 | options = {
50 | enable = mkOption {
51 | type = types.bool;
52 | example = true;
53 | default = false;
54 | description = "Enable/disable IPv6 support for nixcloud.container";
55 | };
56 | ipv6InternetInterfaceAddress = mkOption {
57 | type = types.str;
58 | example = "2001:db8:3c4d:15::1";
59 | default = "";
60 | description = "The IPv6 address which is assigned to the DHCPD6 interface and must be contained in the `ipv6Prefix` as DHCPD6 wouldn't work otherwise.";
61 | };
62 | ipv6Prefix = mkOption {
63 | type = types.str;
64 | default = "";
65 | example = "2001:db8:3c4d:15::";
66 | description = "See https://www.google.de/search?q=ipv6+prefix";
67 | };
68 | ipv6PrefixLength = mkOption {
69 | type = types.int;
70 | example = 48;
71 | default = 128;
72 | description = "See https://www.google.de/search?q=ipv6+prefix";
73 | };
74 | ipv6NameServers = mkOption {
75 | type = types.listOf (types.str);
76 | default = [];
77 | example = [ "2a01:4f8:0:1::add:1010" "2a01:4f8:0:1::add:9999" "2a01:4f8:0:1::add:9898" ];
78 | description = "A list of IPv6 nameservers which are used in the LXC guests to resolve DNS requets via IPv6";
79 | };
80 | };
81 | };
82 | };
83 | };
84 | };
85 | };
86 |
87 | config = mkIf config.nixcloud.container.enable {
88 | virtualisation.lxc.defaultConfig = ''
89 | lxc.id_map = u 0 100000 65536
90 | lxc.id_map = g 0 100000 65536
91 | '';
92 |
93 | virtualisation.lxc.enable = true;
94 |
95 | users.users.root.subGidRanges = [ { count = 65536; startGid = 100000; } ];
96 | users.users.root.subUidRanges = [ { count = 65536; startUid = 100000; } ];
97 |
98 | networking = {
99 | bridges.brNC-hostonly.interfaces = [];
100 | bridges.brNC-internet.interfaces = [];
101 |
102 | interfaces.brNC-hostonly = {
103 | ipv4.addresses = [ { address = "10.101.0.1"; prefixLength = 16; } ];
104 | useDHCP = false;
105 | };
106 | interfaces.brNC-internet = {
107 | ipv4.addresses = [ { address = "10.202.0.1"; prefixLength = 16; } ];
108 | useDHCP = false;
109 | ipv6.addresses = mkIf (config.nixcloud.container.ipv6.enable)
110 | [ { address = config.nixcloud.container.ipv6.ipv6InternetInterfaceAddress; prefixLength = 128; } ];
111 | };
112 | dhcpcd.denyInterfaces = [ "veth" ];
113 | localCommands = mkIf (config.nixcloud.container.ipv6.enable) ''
114 | ip r replace ${config.nixcloud.container.ipv6.ipv6Prefix}/${toString config.nixcloud.container.ipv6.ipv6PrefixLength} dev brNC-internet
115 | '';
116 |
117 | firewall = {
118 | extraCommands = ''
119 | iptables -P FORWARD DROP
120 | iptables -A FORWARD -i brNC-internet -o ${config.nixcloud.container.internetInterface} -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
121 | iptables -A FORWARD -o brNC-internet -i ${config.nixcloud.container.internetInterface} -m state --state ESTABLISHED,RELATED -j ACCEPT
122 | iptables -A FORWARD -p icmp --icmp-type any -j ACCEPT
123 | iptables --table nat --append POSTROUTING --out-interface ${config.nixcloud.container.internetInterface} -j MASQUERADE
124 | # allow dnsmasq (ipv4 NAT DNS resolver)
125 | iptables -A INPUT -p udp -m udp -m multiport -i brNC-internet -j ACCEPT --dports 53
126 | '' + optionalString (config.nixcloud.container.ipv6.enable) ''
127 | # dhcpv6
128 | ip6tables -A INPUT -p tcp -m tcp -m multiport -i brNC-internet -j ACCEPT --dports 546,547
129 | ip6tables -A INPUT -p udp -m udp -m multiport -i brNC-internet -j ACCEPT --dports 546,547
130 | '';
131 | };
132 | };
133 |
134 | # Enable IPv4 forwarding, so we can do masquerading (container ipv4 traffic)
135 | boot.kernel.sysctl."net.ipv4.ip_forward" = true;
136 |
137 | # Enable IPv6 forwarding (container ipv6 traffic)
138 | # FIXME, not all interfaces need forwarding, probably only `internet`
139 | boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = config.nixcloud.container.ipv6.enable;
140 |
141 | services.dnsmasq.enable = true;
142 |
143 | services.dhcpd4 = {
144 | enable = true;
145 | interfaces = [ "brNC-internet" ];
146 | extraConfig = ''
147 | option subnet-mask 255.255.0.0;
148 | option broadcast-address 10.202.255.255;
149 | option routers 10.202.0.1;
150 | option domain-name-servers 10.202.0.1;
151 | subnet 10.202.0.0 netmask 255.255.0.0 {
152 | range 10.202.0.10 10.202.200.200;
153 | }
154 | '';
155 | };
156 |
157 | services.dhcpd6 = mkIf (config.nixcloud.container.ipv6.enable) {
158 | enable = true;
159 | interfaces = [ "brNC-internet" ];
160 | extraConfig = ''
161 | allow client-updates;
162 | update-conflict-detection false;
163 | update-optimization false;
164 | authoritative;
165 | default-lease-time 86400;
166 | preferred-lifetime 80000;
167 | allow leasequery;
168 |
169 | subnet6 ${config.nixcloud.container.ipv6.ipv6Prefix}/${toString config.nixcloud.container.ipv6.ipv6PrefixLength} {
170 | range6 ${config.nixcloud.container.ipv6.ipv6Prefix}/${toString config.nixcloud.container.ipv6.ipv6PrefixLength};
171 | option dhcp6.name-servers ${lib.concatMapStringsSep ", " (x: x) config.nixcloud.container.ipv6.ipv6NameServers};
172 | }
173 | '';
174 | };
175 |
176 | # https://serverfault.com/questions/905332/getting-ipv6-via-radvd-dhcpd6-in-an-lxc-guest-working
177 | services.radvd = mkIf (config.nixcloud.container.ipv6.enable) {
178 | enable = true;
179 | config = ''
180 | interface brNC-internet {
181 | AdvSendAdvert on;
182 | MinRtrAdvInterval 3;
183 | MaxRtrAdvInterval 10;
184 | };
185 | '';
186 | };
187 |
188 | environment.systemPackages = [ pkgs.nixcloud.container pkgs.libuuid ];
189 | nixcloud.tests.wanted = [ ../../tests/container.nix ];
190 |
191 | systemd.services."lxc-autostart" = {
192 | description = "LXC autostart and autostop daemon";
193 |
194 | wantedBy = [ "multi-user.target" ];
195 | serviceConfig = {
196 | ExecStart = "${pkgs.lxc}/bin/lxc-autostart";
197 | ExecStop = "${pkgs.lxc}/bin/lxc-autostart --all --ignore-auto --shutdown --timeout 40";
198 | RemainAfterExit = true;
199 | Type="oneshot";
200 | };
201 | };
202 | };
203 | }
204 |
--------------------------------------------------------------------------------
/modules/web/core/directories.nix:
--------------------------------------------------------------------------------
1 | { lib, config, ... }:
2 |
3 | let
4 | mkDirOption = attrs: import ../../core/directories/options.nix (attrs // {
5 | inherit config lib;
6 | isWebServices = true;
7 | });
8 | in {
9 | # NOTE: Check modules/web/default.nix if you add another option here.
10 | options = lib.mapAttrs (lib.const mkDirOption) {
11 | directories = {
12 | basePath = config.stateDir;
13 | basePathOpt = "stateDir";
14 | };
15 |
16 | runtimeDirectories = {
17 | basePath = config.runtimeDir;
18 | basePathOpt = "runtimeDir";
19 | };
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/modules/web/core/meta.nix:
--------------------------------------------------------------------------------
1 | { options, lib, wsName, ... }:
2 |
3 | {
4 | options.meta = {
5 | license = lib.mkOption {
6 | type = lib.types.attrs;
7 | readOnly = true;
8 | description = let
9 | url = "https://github.com/NixOS/nixpkgs/blob/master/lib/licenses.nix";
10 | in ''
11 | A license attribute from the list found in lib/licenses.nix file
13 | in the upstream nixpkgs source.
14 | '';
15 | };
16 | };
17 |
18 | config.assertions = lib.singleton {
19 | assertion = options.meta.license.isDefined;
20 | message = "No 'meta.license' defined for web service '${wsName}'.";
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/modules/web/core/webserver.nix:
--------------------------------------------------------------------------------
1 | # This submodule contains all of the option declarations and definitions that
2 | # are common among all web server modules.
3 | #
4 | { toplevel, name, config, pkgs, lib, ... }:
5 |
6 | let
7 | needsWebServerInit = config.webserver.init != ""
8 | || config.webserver.startupScript != "";
9 | in {
10 | options = {
11 | proxyOptions = lib.mkOption {
12 | default = {};
13 | type = lib.types.submodule (import ../../services/reverse-proxy/options.nix);
14 | description = "";
15 | example = {
16 | port = 3333;
17 | path = "/tour";
18 | domain = "nixcloud.io";
19 | ip = "127.0.0.1";
20 | };
21 | };
22 |
23 | webserver.systemPackages = lib.mkOption {
24 | type = lib.types.listOf lib.types.path;
25 | default = [];
26 | description = ''
27 | Used to add useful scripts for webservice management into the system
28 | profile by using
29 | '';
30 | };
31 |
32 | webserver.startupScript = lib.mkOption {
33 | type = lib.types.lines;
34 | default = "";
35 | description = ''
36 | Commands that are run prior to starting the actual web server using the
37 | privileges of the user defined in .
38 |
39 | These commands are run directly after
40 | but before the actual
41 | web server.
42 | '';
43 | };
44 |
45 | webserver.init = lib.mkOption {
46 | type = lib.types.lines;
47 | default = "";
48 | description = ''
49 | Commands that are run prior to starting the actual web server.
50 |
51 | These commands are run as the root user.
53 | '';
54 | };
55 |
56 | webserver.user = lib.mkOption {
57 | default = "webserver";
58 | type = lib.types.str;
59 | description = "The main user name which executes this webservice.";
60 | };
61 |
62 | webserver.userOptions = lib.mkOption {
63 | default = {};
64 | type = lib.types.submodule toplevel.options.users.users.type.functor.wrapped.getSubModules;
65 | description = ''
66 | Additional options for the user, see for
67 | possible values.
68 | '';
69 | };
70 |
71 | webserver.group = lib.mkOption {
72 | default = "webserver";
73 | type = lib.types.str;
74 | description = "The main group name which executes this webservice.";
75 | };
76 |
77 | webserver.groupOptions = lib.mkOption {
78 | default = {};
79 | type = lib.types.submodule toplevel.options.users.groups.type.functor.wrapped.getSubModules;
80 | description = ''
81 | Additional options for the group, see for
82 | possible values.
83 | '';
84 | };
85 |
86 | webserver.privateTmp = lib.mkOption {
87 | default = true;
88 | example = false;
89 | description = ''
90 | Weather to force the webservice to use a private
91 | /tmp instance.
92 |
93 |
94 | If postgresql stores the socket context in /tmp
95 | you have to say false here or it can't be used at
96 | all.
97 |
98 | '';
99 | };
100 | };
101 |
102 | config = lib.mkMerge [
103 | (lib.mkIf config.enable {
104 | toplevel.assertions = [
105 | { assertion = config.proxyOptions.path != "";
106 | message = "proxyOptions.path must not be an empty string";
107 | }
108 | { assertion = config.proxyOptions.domain != "";
109 | message = "proxyOptions.domain must not be an empty string";
110 | }
111 | ];
112 |
113 | users.${config.webserver.user} = lib.mkMerge [
114 | (lib.mkForce { inherit (config.webserver) name group; })
115 | (lib.modules.mkAliasAndWrapDefsWithPriority lib.id config.webserver.userOptions)
116 | ];
117 |
118 | groups.${config.webserver.group} = lib.mkMerge [
119 | (lib.mkForce { inherit (config.webserver) name; })
120 | (lib.modules.mkAliasAndWrapDefsWithPriority lib.id config.webserver.groupOptions)
121 | ];
122 | })
123 | (lib.mkIf (config.enable && needsWebServerInit) {
124 | systemd.services.webserver-init = {
125 | description = "Web Server Initialization";
126 | wantedBy = [ "multi-user.target" ];
127 | after = [ "network.target" "fs.target" "keys.target" ];
128 | instance.after = [ "database.target" ];
129 |
130 | serviceConfig.Type = "oneshot";
131 | serviceConfig.RemainAfterExit = true;
132 | } // (if config.webserver.startupScript == "" then {
133 | script = config.webserver.init;
134 | } else {
135 | preStart = config.webserver.init;
136 | script = config.webserver.startupScript;
137 |
138 | serviceConfig.Type = "oneshot";
139 | serviceConfig.User = config.webserver.user;
140 | serviceConfig.Group = config.webserver.group;
141 | serviceConfig.PermissionsStartOnly = true;
142 | serviceConfig.RemainAfterExit = true;
143 | });
144 | })
145 | ];
146 | }
147 |
--------------------------------------------------------------------------------
/modules/web/database/mysql-hook.sh:
--------------------------------------------------------------------------------
1 | export tempDbSocketPath="$TMPDIR/.mysql.sock"
2 | export tempDbPhpHostname="localhost:$tempDbSocketPath"
3 |
4 | escapeSqlString() {
5 | local slashes="${1//\\/\\\\}"
6 | echo "${slashes//\'/\\\'}"
7 | }
8 |
9 | tempdbInit() {
10 | @eatmydata@ mysql_install_db \
11 | --basedir="@mysqlBaseDir@" \
12 | --datadir="$TMPDIR/tempdb" \
13 | --skip-name-resolve
14 | @eatmydata@ mysqld \
15 | --basedir="@mysqlBaseDir@" \
16 | --datadir="$TMPDIR/tempdb" \
17 | --skip-networking \
18 | --socket="$tempDbSocketPath" &
19 |
20 | while [ ! -S "$tempDbSocketPath" ]; do sleep 0.1; done
21 |
22 | local newUser="'$(escapeSqlString "$tempDbUser")'@'localhost'"
23 | local newDb="\`${tempDbName//\`/\\\`}\`"
24 | echo "CREATE USER $newUser; GRANT ALL PRIVILEGES ON $newDb.* TO $newUser;" \
25 | | mysql -uroot --socket="$tempDbSocketPath"
26 | }
27 |
28 | tempdbShell() {
29 | mysql -u "$tempDbUser" --socket="$tempDbSocketPath" "$tempDbName" "$@"
30 | }
31 |
32 | tempdbDump() {
33 | mysqldump -u "$tempDbUser" --socket="$tempDbSocketPath" "$tempDbName" "$@"
34 | }
35 |
--------------------------------------------------------------------------------
/modules/web/database/mysql.nix:
--------------------------------------------------------------------------------
1 | { lib, mkUniqueUser, mkUniqueGroup, pkgs, config, ... }:
2 |
3 | let
4 | filterDb = lib.const (db: db.type == "mysql");
5 | dbs = lib.filterAttrs filterDb config.database;
6 |
7 | inherit (config.mysql) package dataDir;
8 | isMariaDb = lib.hasPrefix "mariadb" package.name;
9 | serverName = if isMariaDb then "MariaDB" else "MySQL";
10 | progName = if isMariaDb then "mariadb" else "mysql";
11 | authMethod = if isMariaDb then "unix_socket" else "auth_socket";
12 |
13 | escapeSql = val: "'${lib.escape ["'" "\\"] val}'";
14 | assertSqlIdent = v: assert builtins.match "[0-9a-zA-Z$_]+" v != null; v;
15 |
16 | mysqlShell = user: database: let
17 | prog = "${package}/bin/mysql";
18 | args = [
19 | "--user=${user}"
20 | "--socket=${configuration.socket}"
21 | database
22 | ];
23 | in "${prog} " + lib.concatMapStringsSep " " lib.escapeShellArg args;
24 |
25 | dbservices = lib.listToAttrs (lib.concatMap (cfg: let
26 | mainUser = mkUniqueUser cfg.user;
27 | owners = lib.singleton mainUser ++ map mkUniqueUser cfg.owners;
28 | mkStateFile = action: let
29 | filename = ".database-${action}-${cfg.name}";
30 | in "${config.stateDir}/${filename}";
31 |
32 | # XXX: Make this dry, because it's also used in a similar vein in
33 | # postgresql.nix!
34 | createDbService = {
35 | name = "database-${cfg.name}";
36 | value = {
37 | instance.requiredBy = [ "database-${cfg.name}.target" ];
38 | instance.before = [ "database-${cfg.name}.target" ];
39 | instance.after = [ "mysql.service" ];
40 | script = let
41 | createDb = pkgs.writeText "create-${cfg.name}.sql" ''
42 | CREATE DATABASE `${assertSqlIdent cfg.name}`;
43 | ${lib.concatMapStrings (uname: ''
44 | CREATE USER IF NOT EXISTS ${escapeSql uname}@'localhost'
45 | IDENTIFIED WITH ${authMethod};
46 | GRANT ALL ON `${assertSqlIdent cfg.name}`.* TO
47 | ${escapeSql uname}@'localhost';
48 | '') owners}
49 | '';
50 | in "${mysqlShell (mkUniqueUser "mysql") "mysql"} < ${createDb}";
51 | postStart = "touch ${lib.escapeShellArg (mkStateFile "create")}";
52 | unitConfig.ConditionPathExists = "!${mkStateFile "create"}";
53 | serviceConfig = {
54 | Type = "oneshot";
55 | RemainAfterExit = true;
56 | PermissionsStartOnly = true;
57 | User = "mysql";
58 | Group = "mysql";
59 | };
60 | };
61 | };
62 |
63 | # XXX: Make this dry, because it's also used in a similar vein in
64 | # postgresql.nix!
65 | postCreateDbService = {
66 | name = "database-${cfg.name}-post-create";
67 | value = {
68 | instance.requiredBy = [ "database-${cfg.name}.target" ];
69 | instance.before = [ "database-${cfg.name}.target" ];
70 | instance.after = [
71 | "mysql.service"
72 | "database-${cfg.name}.service"
73 | ];
74 | script = cfg.postCreate;
75 | path = lib.singleton (pkgs.writeScriptBin "sqlsh" ''
76 | #!${pkgs.stdenv.shell}
77 | exec ${mysqlShell (mkUniqueUser cfg.user) cfg.name} "$@"
78 | '');
79 | postStart = "touch ${lib.escapeShellArg (mkStateFile "post-create")}";
80 | unitConfig.ConditionPathExists = "!${mkStateFile "post-create"}";
81 | serviceConfig = {
82 | Type = "oneshot";
83 | RemainAfterExit = true;
84 | PermissionsStartOnly = true;
85 | User = cfg.user;
86 | };
87 | };
88 | };
89 |
90 | services = lib.singleton createDbService
91 | ++ lib.optional (cfg.postCreate != "") postCreateDbService;
92 |
93 | in services) (lib.attrValues dbs));
94 |
95 | configuration = {
96 | basedir = package;
97 | datadir = dataDir;
98 | log-output = "NONE";
99 | skip-networking = true;
100 | socket = "${config.runtimeDir}/.mysql.sock";
101 | plugin-load-add = "auth_socket.so";
102 | };
103 |
104 | cfgVals = let
105 | mkCfgVal = name: val: let
106 | flag = "--${name}" + lib.optionalString (val != true) "=${val}";
107 | in lib.escapeShellArg flag;
108 | in lib.mapAttrsToList mkCfgVal configuration;
109 |
110 | cmdLine = lib.concatStringsSep " " cfgVals;
111 |
112 | in {
113 | options.mysql = {
114 | package = lib.mkOption {
115 | type = lib.types.package;
116 | default = pkgs.mariadb;
117 | example = lib.literalExample "pkgs.mysql57";
118 | description = "MySQL package to use.";
119 | };
120 |
121 | dataDir = lib.mkOption {
122 | type = lib.types.path;
123 | default = "${config.stateDir}/mysql";
124 | readOnly = true;
125 | description = "Data directory for MySQL";
126 | };
127 | };
128 |
129 | config = lib.mkIf (dbs != {} && config.enable) {
130 | users.mysql = {
131 | group = "mysql";
132 | description = "${serverName} Server User";
133 | };
134 |
135 | groups.mysql = {};
136 |
137 | dbShellCommand.mysql = ''
138 | exec ${lib.escapeShellArg "${package}/bin/mysql"} \
139 | --user="$1" \
140 | --socket=${lib.escapeShellArg configuration.socket} \
141 | "$2"
142 | '';
143 |
144 | tempDbSetupHook.dependencies = [ package ];
145 | tempDbSetupHook.script = ./mysql-hook.sh;
146 | tempDbSetupHook.substitutions.mysqlBaseDir = package;
147 |
148 | directories.mysql = {
149 | instance.before = [ "mysql-initdb.service" ];
150 | permissions.defaultDirectoryMode = "0711";
151 | permissions.group.noAccess = true;
152 | permissions.others.noAccess = true;
153 | permissions.enableACLs = false;
154 | owner = mkUniqueUser "mysql";
155 | group = mkUniqueGroup "mysql";
156 | };
157 |
158 | systemd.services = {
159 | mysql-initdb = {
160 | description = "Initialize ${serverName} Server";
161 | instance.requiredBy = [ "mysql.service" ];
162 | instance.before = [ "mysql.service" ];
163 | unitConfig.ConditionPathExists = "!${dataDir}/mysql";
164 | serviceConfig = {
165 | ExecStart = "${package}/bin/mysql_install_db ${cmdLine}";
166 | Type = "oneshot";
167 | RemainAfterExit = true;
168 | PermissionsStartOnly = true;
169 | User = "mysql";
170 | Group = "mysql";
171 | };
172 | };
173 |
174 | mysql = {
175 | description = "${serverName} Server";
176 | instance.requiredBy = [ "db-server.target" ];
177 | instance.before = [ "db-server.target" ];
178 | after = [ "network.target" ];
179 | postStart = let
180 | superuser = escapeSql (mkUniqueUser "mysql");
181 | dropRoot = pkgs.writeText "drop-root.sql" ''
182 | CREATE USER ${superuser}@'localhost' IDENTIFIED WITH ${authMethod};
183 | GRANT ALL ON *.* TO ${superuser}@'localhost' WITH GRANT OPTION;
184 | DROP DATABASE test;
185 | DELETE FROM mysql.user WHERE user IN ('root', ''');
186 | FLUSH PRIVILEGES;
187 | '';
188 | in ''
189 | if [ ! -e ${lib.escapeShellArg dataDir}/.root-dropped ]; then
190 | ${mysqlShell "root" "mysql"} < ${dropRoot}
191 | touch ${lib.escapeShellArg dataDir}/.root-dropped
192 | fi
193 | '';
194 | serviceConfig = {
195 | ExecStart = "@${package}/bin/mysqld ${progName} ${cmdLine}";
196 | User = "mysql";
197 | Group = "mysql";
198 | Type = "notify";
199 | };
200 | };
201 | } // dbservices;
202 | };
203 | }
204 |
--------------------------------------------------------------------------------
/modules/web/database/postgresql-hook.sh:
--------------------------------------------------------------------------------
1 | export PGHOST="$TMPDIR"
2 | export tempDbSocketPath="$PGHOST"
3 | export tempDbPhpHostname="$PGHOST"
4 |
5 | escapeSqlString() {
6 | echo "${1//\'/\'\'}"
7 | }
8 |
9 | tempdbInit() {
10 | initdb -D "$TMPDIR/tempdb" -E UTF8 -N -U "$tempDbUser"
11 | [ -z "$createTempDb" ] || createdb "$tempDbName"
12 | pg_ctl start -w -D "$TMPDIR/tempdb" -o \
13 | "-F --listen_addresses= --unix_socket_directories=$tempDbSocketPath"
14 | }
15 |
16 | tempdbShell() {
17 | psql "$tempDbName" "$tempDbUser" "$@"
18 | }
19 |
20 | tempdbDump() {
21 | pg_dump -U "$tempDbUser" "$tempDbName" "$@"
22 | }
23 |
--------------------------------------------------------------------------------
/modules/web/database/postgresql.nix:
--------------------------------------------------------------------------------
1 | { lib, mkUniqueUser, mkUniqueGroup, pkgs, config, ... }:
2 |
3 | let
4 | filterDb = lib.const (db: db.type == "postgresql");
5 | dbs = lib.filterAttrs filterDb config.database;
6 |
7 | package = config.postgresql.package.overrideAttrs (drv: {
8 | configureFlags = (drv.configureFlags or []) ++ [ "--with-systemd" ];
9 | buildInputs = (drv.buildInputs or []) ++ [ pkgs.systemd ];
10 | });
11 |
12 | mkMapName = database: "db-owners-${builtins.hashString "sha256" database}";
13 |
14 | # This maps additional owners to the user which is the main owner of the
15 | # database.
16 | pgIdent = pkgs.writeText "pg_ident.conf" (lib.concatMapStrings (cfg: ''
17 | ${mkMapName cfg.name} "${mkUniqueUser cfg.user}" "${mkUniqueUser cfg.user}"
18 | ${lib.concatMapStrings (owner: ''
19 | ${mkMapName cfg.name} "${mkUniqueUser owner}" "${mkUniqueUser cfg.user}"
20 | '') cfg.owners}
21 | '') (lib.attrValues dbs));
22 |
23 | dbservices = lib.listToAttrs (lib.concatMap (cfg: let
24 | dbuser = mkUniqueUser cfg.user;
25 | mkStateFile = action: let
26 | filename = ".database-${action}-${cfg.name}";
27 | in "${config.stateDir}/${filename}";
28 |
29 | # XXX: Make this dry, because it's also used in a similar vein in
30 | # mysql.nix!
31 | createDbService = {
32 | name = "database-${cfg.name}";
33 | value = {
34 | instance.requiredBy = [ "database-${cfg.name}.target" ];
35 | instance.before = [ "database-${cfg.name}.target" ];
36 | instance.after = [ "postgresql.service" ];
37 | environment.PGHOST = cfg.socketPath;
38 | script = ''
39 | ${package}/bin/createuser ${lib.escapeShellArg dbuser}
40 | ${package}/bin/createdb ${lib.escapeShellArg cfg.name} \
41 | -O ${lib.escapeShellArg dbuser}
42 | '';
43 | postStart = "touch ${lib.escapeShellArg (mkStateFile "create")}";
44 | unitConfig.ConditionPathExists = "!${mkStateFile "create"}";
45 | serviceConfig = {
46 | Type = "oneshot";
47 | RemainAfterExit = true;
48 | PermissionsStartOnly = true;
49 | User = "postgres";
50 | Group = "postgres";
51 | };
52 | };
53 | };
54 |
55 | # XXX: Make this dry, because it's also used in a similar vein in
56 | # mysql.nix!
57 | postCreateDbService = {
58 | name = "database-${cfg.name}-post-create";
59 | value = {
60 | instance.requiredBy = [ "database-${cfg.name}.target" ];
61 | instance.before = [ "database-${cfg.name}.target" ];
62 | instance.after = [
63 | "postgresql.service"
64 | "database-${cfg.name}.service"
65 | ];
66 | environment.PGHOST = cfg.socketPath;
67 | script = cfg.postCreate;
68 | path = lib.singleton (pkgs.writeScriptBin "sqlsh" ''
69 | #!${pkgs.stdenv.shell}
70 | exec ${package}/bin/psql ${lib.escapeShellArg cfg.name} "$@"
71 | '');
72 | postStart = "touch ${lib.escapeShellArg (mkStateFile "post-create")}";
73 | unitConfig.ConditionPathExists = "!${mkStateFile "post-create"}";
74 | serviceConfig = {
75 | Type = "oneshot";
76 | RemainAfterExit = true;
77 | PermissionsStartOnly = true;
78 | User = cfg.user;
79 | };
80 | };
81 | };
82 |
83 | services = lib.singleton createDbService
84 | ++ lib.optional (cfg.postCreate != "") postCreateDbService;
85 |
86 | in services) (lib.attrValues dbs));
87 |
88 | inherit (config.postgresql) dataDir;
89 | inherit (package) psqlSchema;
90 |
91 | configuration = {
92 | hba_file = pkgs.writeText "pg_hba.conf" (lib.concatMapStrings (cfg: ''
93 | local "${cfg.name}" all peer map=${mkMapName cfg.name}
94 | '') (lib.attrValues dbs) + ''
95 | local all all peer
96 | '');
97 | unix_socket_directories = config.runtimeDir;
98 | log_destination = "stderr";
99 | port = "5432";
100 | listen_addresses = "";
101 | ident_file = pgIdent;
102 | };
103 |
104 | in {
105 | options.postgresql = {
106 | package = lib.mkOption {
107 | type = lib.types.package;
108 | default = pkgs.postgresql96;
109 | example = lib.literalExample "pkgs.postgresql96";
110 | description = "PostgreSQL package to use.";
111 | };
112 |
113 | dataDir = lib.mkOption {
114 | type = lib.types.path;
115 | default = "${config.stateDir}/postgresql/${psqlSchema}";
116 | readOnly = true;
117 | description = "Data directory for PostgreSQL";
118 | };
119 | };
120 |
121 | config = lib.mkIf (dbs != {} && config.enable) {
122 | users.postgres = {
123 | group = "postgres";
124 | description = "PostgreSQL Server User";
125 | };
126 |
127 | groups.postgres = {};
128 |
129 | tempDbSetupHook.dependencies = [ package ];
130 | tempDbSetupHook.script = ./postgresql-hook.sh;
131 |
132 | dbShellCommand.postgresql = ''
133 | export PGHOST=${lib.escapeShellArg config.runtimeDir}
134 | exec ${lib.escapeShellArg "${package}/bin/psql"} "$2"
135 | '';
136 |
137 | directories."postgresql/${psqlSchema}" = {
138 | instance.before = [ "postgresql-initdb.service" ];
139 | permissions.defaultDirectoryMode = "0711";
140 | permissions.group.noAccess = true;
141 | permissions.others.noAccess = true;
142 | permissions.enableACLs = false;
143 | owner = mkUniqueUser "postgres";
144 | group = mkUniqueGroup "postgres";
145 | };
146 |
147 | systemd.services = {
148 | postgresql-initdb = {
149 | description = "Initialize PostgreSQL Cluster";
150 | instance.requiredBy = [ "postgresql.service" ];
151 | instance.before = [ "postgresql.service" ];
152 | environment.PGDATA = dataDir;
153 | unitConfig.ConditionPathExists = "!${dataDir}/PG_VERSION";
154 | serviceConfig = {
155 | ExecStart = "${package}/bin/initdb";
156 | Type = "oneshot";
157 | RemainAfterExit = true;
158 | PermissionsStartOnly = true;
159 | User = "postgres";
160 | Group = "postgres";
161 | };
162 | };
163 |
164 | postgresql = {
165 | description = "PostgreSQL Server";
166 | instance.requiredBy = [ "db-server.target" ];
167 | instance.before = [ "db-server.target" ];
168 | after = [ "network.target" ];
169 | environment.PGDATA = dataDir;
170 | serviceConfig = {
171 | ExecStart = let
172 | mkCfgVal = name: val: "-c ${lib.escapeShellArg "${name}=${val}"}";
173 | cfgVals = lib.mapAttrsToList mkCfgVal configuration;
174 | in "${package}/bin/postgres ${lib.concatStringsSep " " cfgVals}";
175 | User = "postgres";
176 | Group = "postgres";
177 | Type = "notify";
178 | };
179 | };
180 | } // dbservices;
181 | };
182 | }
183 |
--------------------------------------------------------------------------------
/modules/web/default.nix:
--------------------------------------------------------------------------------
1 | { config, options, lib, nclib, ... }:
2 |
3 | {
4 | imports = let
5 | # Special submodule without implementing a specific service.
6 | customModule = import ../../lib/make-webservice.nix "custom" {
7 | config = {};
8 | meta.license = null;
9 | };
10 | in (lib.mapAttrsToList (import ../../lib/make-webservice.nix) {
11 | apache = services/apache;
12 | nginx = services/nginx;
13 | filesender = services/filesender;
14 | hydra = services/hydra;
15 | leaps = services/leaps;
16 | mediawiki = services/mediawiki;
17 | static-darkhttpd = services/static-darkhttpd;
18 | static-nginx = services/static-nginx;
19 | mattermost = services/mattermost;
20 | roundcube = services/roundcube;
21 | }) ++ lib.singleton customModule;
22 |
23 | config = let
24 | # A list of all the "toplevel" option definitions of all web services.
25 | toplevel = let
26 | getConfig = lib.mapAttrsToList (lib.const (cfg: cfg.toplevel));
27 | mapTL = w: lib.concatLists (lib.mapAttrsToList (lib.const getConfig) w);
28 | in import ../../lib/make-toplevel-config.nix {
29 | inherit config options lib;
30 | } mapTL [ "nixcloud" "webservices" ];
31 |
32 | # Pass a sub-option that affects the top-level nixcloud namespace to the
33 | # top-level with the ability to rename it. The first argument is the
34 | # attribute path of the top-level option whereas the second argument is the
35 | # option inside nixcloud.webservices that should be passed to the top-level
36 | # option.
37 | passSubOption = outer: inner: let
38 | cfgList = nclib.mapWSConfigToList (lib.getAttrFromPath inner);
39 | in lib.setAttrByPath outer (lib.mkMerge cfgList);
40 |
41 | tests = passSubOption [ "nixcloud" "tests" "wanted" ] [ "tests" "wanted" ];
42 | tls = passSubOption [ "nixcloud" "TLS" "certs" ] [ "TLS" "certs" ];
43 |
44 | # Special case: We don't want to pass the options to the top-level
45 | # unmodified because we want to remove all the web service-specific
46 | # attributes as those are already merged in the config attribute of the
47 | # directories submodule.
48 | dirs.nixcloud.directories = let
49 | modifyConfig = cfg: let
50 | removeInstance = lib.flip removeAttrs [ "instance" ];
51 | combined = cfg.directories // cfg.runtimeDirectories;
52 | in lib.mapAttrs (lib.const removeInstance) combined;
53 | in lib.mkMerge (nclib.mapWSConfigToList modifyConfig);
54 |
55 | in lib.mkMerge [ toplevel tests tls dirs ];
56 | }
57 |
--------------------------------------------------------------------------------
/modules/web/messaging/default.nix:
--------------------------------------------------------------------------------
1 | {
2 | imports = [ ./rabbitmq ];
3 | }
4 |
--------------------------------------------------------------------------------
/modules/web/messaging/rabbitmq/default.nix:
--------------------------------------------------------------------------------
1 | { lib, mkUnique, mkUniqueUser, mkUniqueGroup, nclib, pkgs, config, ... }:
2 |
3 | let
4 | inherit (config.messaging.rabbitmq) package dataDir;
5 |
6 | epmdRules = [
7 | { port = 4369;
8 | type = "tcp";
9 | direction = "outgoing";
10 | socketPath = "${config.runtimeDir}/epmd.socket";
11 | }
12 | # This rule is to make sure that the process is not starting another epmd
13 | # and instead usees the one provided by the systemd service.
14 | { port = 4369;
15 | type = "tcp";
16 | direction = "incoming";
17 | reject = true;
18 | rejectError = "EADDRINUSE";
19 | }
20 | { port = 0;
21 | socketPath = "${config.runtimeDir}/.epmd-rabbitmq-%p.socket";
22 | }
23 | { socketPath = "/tmp/xxx-%a-%t-%p.socket"; }
24 | ];
25 |
26 | rabbitmqEnvFile = let
27 | mkEnvVar = name: val: "${name}=${lib.escapeShellArg val}";
28 | mkEnvVars = lib.mapAttrsToList mkEnvVar;
29 | mkEnvConf = attrs: lib.concatStringsSep "\n" (mkEnvVars attrs) + "\n";
30 | in pkgs.writeText "rabbitmq-env.sh" (mkEnvConf {
31 | CONFIG_FILE = let
32 | rabbit37 = lib.versionAtLeast package.version "3.7.0";
33 | oldConfig = pkgs.writeText "rabbitmq.config" ''
34 | [{rabbit, [{loopback_users, []}]}].
35 | '';
36 | in if rabbit37 then pkgs.writeText "rabbitmq.conf" ''
37 | loopback_users = none
38 | '' else lib.removeSuffix ".config" oldConfig;
39 | LOG_BASE = "${dataDir}/log";
40 | MNESIA_BASE = "${dataDir}/mnesia";
41 | ENABLED_PLUGINS_FILE = pkgs.writeText "rabbitmq-enabled-plugins" "";
42 | GENERATED_CONFIG_DIR = "${dataDir}/config";
43 | SCHEMA_DIR = "${dataDir}/schema";
44 | SERVER_ADDITIONAL_ERL_ARGS = "-setcookie NOT_NEEDED";
45 | CTL_ERL_ARGS = "-setcookie NOT_NEEDED";
46 | });
47 |
48 | wrappedCtl = nclib.ip2unix {
49 | program = "${package}/bin/rabbitmqctl";
50 | baseName = mkUnique "rabbitmqctl";
51 | makeWrapperArgs = [
52 | "--set" "RABBITMQ_CONF_ENV_FILE" (toString rabbitmqEnvFile)
53 | ];
54 | useBinDir = true;
55 | rules = [
56 | { port = 25672;
57 | type = "tcp";
58 | direction = "outgoing";
59 | socketPath = "${config.runtimeDir}/rabbitmqctl.socket";
60 | }
61 | { port = 35672;
62 | portEnd = 35682;
63 | direction = "incoming";
64 | socketPath = "${config.runtimeDir}/rabbitmqctl-%p.socket";
65 | }
66 | ] ++ epmdRules;
67 | };
68 |
69 | in {
70 | options.messaging.rabbitmq = {
71 | enable = lib.mkEnableOption "RabbitMQ AMQP broker";
72 |
73 | package = lib.mkOption {
74 | type = lib.types.package;
75 | default = pkgs.rabbitmq_server or pkgs.rabbitmq-server;
76 | example = lib.literalExample "pkgs.rabbitmq-server";
77 | description = "RabbitMQ package to use.";
78 | };
79 |
80 | dataDir = lib.mkOption {
81 | type = lib.types.path;
82 | default = "${config.stateDir}/rabbitmq";
83 | readOnly = true;
84 | description = "Data directory for RabbitMQ.";
85 | };
86 | };
87 |
88 | config = lib.mkIf (config.enable && config.messaging.rabbitmq.enable) {
89 | users.rabbitmq = {
90 | group = "rabbitmq";
91 | description = "RabbitMQ User";
92 | home = dataDir;
93 | createHome = true;
94 | };
95 |
96 | tools.useRabbitMQ = nclib.ip2unix {
97 | baseName = "rabbitmq-client";
98 | rules = lib.singleton {
99 | port = 5672;
100 | type = "tcp";
101 | direction = "outgoing";
102 | socketPath = "${config.runtimeDir}/rabbitmq.socket";
103 | };
104 | };
105 |
106 | groups.rabbitmq = {};
107 |
108 | directories.rabbitmq = {
109 | instance.before = [ "rabbitmq.service" "epmd.socket" ];
110 | permissions.defaultDirectoryMode = "0711";
111 | permissions.group.noAccess = true;
112 | permissions.others.noAccess = true;
113 | permissions.enableACLs = false;
114 | owner = mkUniqueUser "rabbitmq";
115 | group = mkUniqueGroup "rabbitmq";
116 | };
117 |
118 | # TODO: Move into own module once we have more Erlang services.
119 | systemd.sockets.epmd = {
120 | description = "Erlang Port Mapper Daemon Socket";
121 | instance.requiredBy = [ "instance-init.target" ];
122 | unitConfig.DefaultDependencies = false;
123 | socketConfig.ListenStream = "${config.runtimeDir}/epmd.socket";
124 | socketConfig.FileDescriptorName = "epmd";
125 | };
126 |
127 | systemd.services.epmd = {
128 | description = "Erlang Port Mapper Daemon";
129 | # While epmd has the ability to use systemd socket activation, we instead
130 | # use the one by ip2unix, because the implementation of epmd expects IP
131 | # sockets.
132 | serviceConfig.ExecStart = "${nclib.ip2unix {
133 | program = "${pkgs.erlang}/bin/epmd";
134 | rules = [
135 | { port = 4369;
136 | type = "tcp";
137 | address = "127.0.0.1";
138 | direction = "incoming";
139 | socketActivation = true;
140 | fdName = "epmd";
141 | }
142 | { direction = "incoming";
143 | blackhole = true;
144 | }
145 | # Needed for epmd to check whether the node is alive.
146 | #{ direction = "outgoing";
147 | # socketPath = "${config.runtimeDir}/.epmd-rabbitmq-%p.socket";
148 | #}
149 | ];
150 | }} -address 127.0.0.1";
151 | serviceConfig.DynamicUser = true;
152 | };
153 |
154 | systemd.services.rabbitmq = {
155 | description = "RabbitMQ Server";
156 | instance.requiredBy = [ "instance-init.target" ];
157 |
158 | # Needed for startup notifications
159 | path = [ pkgs.socat ];
160 |
161 | environment.RABBITMQ_CONF_ENV_FILE = rabbitmqEnvFile;
162 |
163 | serviceConfig = {
164 | ExecStart = nclib.ip2unix {
165 | program = "${package}/bin/rabbitmq-server";
166 | rules = [
167 | { port = 5671;
168 | portEnd = 5672;
169 | type = "tcp";
170 | direction = "incoming";
171 | socketPath = "${config.runtimeDir}/rabbitmq.socket";
172 | }
173 | { type = "tcp";
174 | port = 25672;
175 | direction = "incoming";
176 | socketPath = "${config.runtimeDir}/rabbitmqctl.socket";
177 | }
178 | { port = 35672;
179 | portEnd = 35682;
180 | direction = "outgoing";
181 | socketPath = "${config.runtimeDir}/rabbitmqctl-%p.socket";
182 | }
183 | ] ++ epmdRules;
184 | };
185 | ExecStop = lib.escapeShellArgs [
186 | "${wrappedCtl}/bin/${mkUnique "rabbitmqctl"}" "stop"
187 | ];
188 | User = "rabbitmq";
189 | Group = "rabbitmq";
190 | Type = "notify";
191 | NotifyAccess = "all";
192 | };
193 | };
194 |
195 | tests.wanted = [ ./test.nix ];
196 |
197 | # FIXME: This shouldn't be "webserver." and we need a generic way to pass
198 | # through helper programs.
199 | webserver.systemPackages = [ wrappedCtl ];
200 | };
201 | }
202 |
--------------------------------------------------------------------------------
/modules/web/messaging/rabbitmq/test.nix:
--------------------------------------------------------------------------------
1 | let
2 | rabbitMQWebService = { config, pkgs, ... }: let
3 | # FIXME: Use python3Packages here as soon as pika 0.13.0 hits the nixos-
4 | # unstable channel, because it fixes support with Python 3.7.
5 | inherit (pkgs.python36Packages) buildPythonApplication pika;
6 | inherit (config.tools) useRabbitMQ;
7 | mkPyTest = name: src: buildPythonApplication {
8 | name = "test-rabbitmq-${name}";
9 | propagatedBuildInputs = [ pika ];
10 | postPatch = ''
11 | cat > setup.py <waitForUnit('rabbitmq-test-foo-rabbitmq.service');
82 |
83 | subtest "check whether stop/start works", sub {
84 | $machine->succeed('systemctl stop rabbitmq-test-foo-rabbitmq.service');
85 | $machine->succeed('systemctl start rabbitmq-test-foo-rabbitmq.service');
86 | $machine->waitForUnit('rabbitmq-test-foo-rabbitmq.service');
87 | };
88 |
89 | subtest "sending and receiving message", sub {
90 | $machine->succeed(
91 | 'systemctl start --no-block rabbitmq-test-foo-test-recv >&2',
92 | 'while [ ! -e /tmp/recv-ready ]; do sleep 0.1; done',
93 | 'rabbitmq-test-foo-rabbitmqctl list_queues | grep -q hello',
94 | 'systemctl start rabbitmq-test-foo-test-send >&2',
95 | );
96 | $machine->waitForUnit('rabbitmq-test-foo-test-recv.service');
97 | $machine->succeed('test "$(< /tmp/received)" = world');
98 | };
99 |
100 | subtest "make sure EPMD is not listening", sub {
101 | $machine->fail('nc -z 127.0.0.1 4369');
102 | };
103 | '';
104 | }
105 |
--------------------------------------------------------------------------------
/modules/web/services/apache/default.nix:
--------------------------------------------------------------------------------
1 | { config, options, lib, mkUniqueUser, mkUniqueGroup, ... }:
2 | {
3 | config = lib.mkMerge [
4 | {
5 | directories.www = {
6 | owner = mkUniqueUser config.webserver.user;
7 | group = mkUniqueGroup config.webserver.group;
8 | instance.before = [ "webserver-init.service" "instance-init.target" ];
9 | postCreate = ''
10 | cat > index.html <
12 |
13 |
14 |
15 | Nothing here yet?
16 |
17 |
18 | Nothing here yet?
19 | You can place files into
20 | ${config.stateDir}/www
.
21 |
22 |
23 | EOF
24 |
25 | cat > index.php <
27 |
28 |
29 |
30 | Nothing here yet?
31 |
32 |
33 | HelloWorld from php
'; ?>
34 | You can place files into
35 | ${config.stateDir}/www
.
36 |
37 |
38 | EOF
39 | '';
40 | };
41 | }
42 | {
43 | webserver.variant = "apache";
44 | tests.wanted = [ ./test.nix ];
45 | }
46 | ];
47 |
48 | meta = {
49 | description = "Declarative apache backend implementation for hacking";
50 | maintainers = with lib.maintainers; [ qknight ];
51 | license = lib.licenses.bsd2;
52 | homepage = https://github.com/nixcloud/nixcloud-webservices;
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/modules/web/services/apache/test.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "apache";
3 | machine.nixcloud.reverse-proxy.enable = true;
4 | machine.nixcloud.reverse-proxy.extendEtcHosts = true;
5 | machine.nixcloud.webservices.apache = {
6 | foo = {
7 | enable = true;
8 | proxyOptions = {
9 | domain = "example.com";
10 | http.mode = "on";
11 | https.mode = "off";
12 | port = 8080;
13 | };
14 | webserver.apache = {
15 | enablePHP = true;
16 | phpOptions = ''
17 | # foo's config....
18 | '';
19 | extraConfig = ''
20 | DocumentRoot "/var/lib/nixcloud/webservices/apache-foo/www/"
21 |
22 | Options FollowSymLinks
23 | AllowOverride None
24 | Require all granted
25 |
26 | '';
27 | };
28 | };
29 | };
30 |
31 | testScript = ''
32 | $machine->waitForUnit('multi-user.target');
33 | $machine->waitForOpenPort(80);
34 | $machine->succeed('curl http://example.com/index.php | grep -qF HelloWorld');
35 | '';
36 | }
37 |
--------------------------------------------------------------------------------
/modules/web/services/hydra/test.nix:
--------------------------------------------------------------------------------
1 | let
2 | testRunner = ''
3 | import time
4 | import requests
5 |
6 | session = requests.session()
7 | session.headers.update({
8 | 'referer': 'http://example.com/',
9 | 'content-type': 'application/json',
10 | })
11 |
12 | session.post('http://example.com/login', json={
13 | 'username': 'admin',
14 | 'password': 'test',
15 | }).raise_for_status()
16 |
17 | session.put('http://example.com/project/test', json={
18 | 'displayname': 'Test project',
19 | 'enabled': 1,
20 | }).raise_for_status()
21 |
22 | session.put('http://example.com/jobset/test/foo', json={
23 | 'nixexprpath': 'default.nix',
24 | 'nixexprinput': 'testinput',
25 | 'enabled': 1,
26 | 'checkinterval': 100000,
27 | 'inputs': {
28 | 'testinput': {
29 | 'type': 'git',
30 | 'value': '/var/lib/hydra-test-jobset',
31 | },
32 | },
33 | }).raise_for_status()
34 |
35 | evals = []
36 |
37 | while len(evals) == 0:
38 | response = session.get('http://example.com/jobset/test/foo/evals')
39 | response.raise_for_status()
40 | evals = response.json()['evals']
41 | time.sleep(1)
42 |
43 | build_data = None
44 |
45 | while True:
46 | build = evals[0]['builds'][0]
47 | response = session.get('http://example.com/build/{}'.format(build))
48 | response.raise_for_status()
49 | build_data = response.json()
50 | if build_data['finished'] == 1:
51 | print(build)
52 | break
53 | time.sleep(1)
54 |
55 | assert build_data['job'] == 'testJob'
56 | assert build_data['nixname'] == 'test-job'
57 | assert build_data['buildstatus'] == 0
58 | '';
59 |
60 | in {
61 | name = "hydra";
62 |
63 | machine = { pkgs, lib, ... }: {
64 | nixcloud.reverse-proxy.enable = true;
65 | nixcloud.reverse-proxy.extendEtcHosts = true;
66 | nixcloud.webservices.hydra = {
67 | foo.enable = true;
68 | foo.initialAdminPassword = "test";
69 | foo.proxyOptions.domain = "example.com";
70 | foo.proxyOptions.http.mode = "on";
71 | foo.proxyOptions.https.mode = "off";
72 | foo.proxyOptions.port = 8080;
73 | };
74 | # Set a time zone, because otherwise Hydra will flood us with warnings.
75 | time.timeZone = "UTC";
76 | virtualisation.memorySize = 1024;
77 | environment.systemPackages = let
78 | runner = pkgs.python3Packages.buildPythonApplication {
79 | name = "hydra-test-runner";
80 | propagatedBuildInputs = [ pkgs.python3Packages.requests ];
81 | src = pkgs.runCommand "hydra-test-runner-source" {
82 | inherit testRunner;
83 | } ''
84 | mkdir "$out"
85 | cat > "$out/setup.py" < "$out/hydra-test-runner"
90 | echo -n "$testRunner" >> "$out/hydra-test-runner"
91 | '';
92 | };
93 | in lib.singleton runner;
94 | system.activationScripts.hydrajobset = let
95 | gitRepo = pkgs.runCommand "test-input" {
96 | nativeBuildInputs = [ pkgs.git ];
97 | } ''
98 | mkdir "$out"
99 | export HOME="$PWD"
100 | cd "$out"
101 |
102 | git init
103 | git config --global user.email "dummy@example.com"
104 | git config --global user.name dummy
105 |
106 | cat > default.nix < builder.sh < "\$out"
119 | EOF
120 | chmod +x builder.sh
121 |
122 | git add .
123 | git commit -m initial
124 | '';
125 | in ''
126 | mkdir -p /var/lib
127 | cp -rd ${lib.escapeShellArg gitRepo} /var/lib/hydra-test-jobset
128 | '';
129 | };
130 |
131 | testScript = ''
132 | $machine->waitForUnit('multi-user.target');
133 |
134 | $machine->waitForOpenPort(80);
135 | $machine->waitForOpenPort(8080); # XXX: Should use socket activation!
136 |
137 | my $buildNo = $machine->succeed('hydra-test-runner');
138 | chomp $buildNo;
139 |
140 | $machine->succeed(
141 | "curl -o build.nar http://example.com/build/$buildNo/output/out",
142 | 'xzgrep -qF "hello world" build.nar'
143 | );
144 | '';
145 | }
146 |
--------------------------------------------------------------------------------
/modules/web/services/leaps/default.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, mkUniqueUser, mkUniqueGroup, ... }:
2 | {
3 | options = {};
4 |
5 | meta = {
6 | description = "A pair programming tool and library written in Golang";
7 | homepage = "https://github.com/jeffail/leaps/";
8 | license = lib.licenses.mit;
9 | maintainers = with lib.maintainers; [ qknight ];
10 | meta.platforms = lib.platforms.linux;
11 | };
12 |
13 | config = lib.mkIf config.enable {
14 |
15 | # inject the leaps websocket for cooperative document opening/editing into proxyOptions
16 | proxyOptions.websockets = {
17 | ws = {
18 | subpath = "/leaps/ws";
19 | };
20 | };
21 |
22 | directories.www.postCreate = ''
23 | cat > README.md <succeed('stat -c %'.$flag.' '.$path);
25 | chomp $result;
26 | die "$desc for path $path is $result but expected $expect"
27 | unless $result eq $expect;
28 | }
29 |
30 | sub ensureOwner ($$) {
31 | ensureStat $_[0], $_[1], 'owner', 'U';
32 | }
33 |
34 | sub ensureGroup ($$) {
35 | ensureStat $_[0], $_[1], 'group', 'G';
36 | }
37 |
38 | $machine->waitForUnit('multi-user.target');
39 | $machine->waitForOpenPort(80);
40 | $machine->waitForOpenPort(8080);
41 | $machine->waitForOpenPort(8081);
42 | $machine->succeed('curl http://example.com/ | grep -qF leaps_logo.png');
43 | $machine->succeed('curl http://example.org/ | grep -qF leaps_logo.png');
44 |
45 | ensureOwner "/var/lib/nixcloud/webservices/leaps-foo/www", "leaps-foo";
46 | ensureGroup "/var/lib/nixcloud/webservices/leaps-foo/www", "leaps-foo";
47 |
48 | ensureOwner "/var/lib/nixcloud/webservices/leaps-bar/www", "leaps-bar";
49 | ensureGroup "/var/lib/nixcloud/webservices/leaps-bar/www", "leaps-bar";
50 | '';
51 | }
52 |
--------------------------------------------------------------------------------
/modules/web/services/mattermost/default.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, mkUniqueUser, mkUniqueGroup, ... }:
2 |
3 | let
4 | inherit (lib) mkOption types;
5 |
6 | # The main binary for Mattermost has changed from (mattermost-)platform to
7 | # just "mattermost".
8 | is5orNewer = let
9 | inherit (builtins.parseDrvName pkgs.mattermost.name) version;
10 | in lib.versionAtLeast version "5.0";
11 | mainBinary = if is5orNewer then "mattermost" else "platform";
12 |
13 | path = builtins.toPath "/${config.proxyOptions.domain}/${config.proxyOptions.path}";
14 | siteUrl = "${if (config.proxyOptions.https.mode == "on") then "https" else "http"}:/${path}";
15 |
16 | mattermostConfig = {
17 | ServiceSettings.SiteURL = "${siteUrl}"; # "https://chat.example.com";
18 | ServiceSettings.ListenAddress = "localhost:${toString config.proxyOptions.port}";
19 | TeamSettings = {
20 | SiteName = config.siteName;
21 | ExperimentalDefaultChannels = [];
22 | };
23 | SqlSettings = {
24 | DriverName = "postgres";
25 | DataSource = "postgres:///mattermost?host=${config.database.mattermost.socketPath}";
26 | # SECURITY/FIXME: hardcoded
27 | AtRestEncryptKey = "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QVg";
28 | };
29 | FileSettings = {
30 | # SECURITY/FIXME: hardcoded
31 | PublicLinkSalt = "A705AklYF8MFDOfcwh3I488G8vtLlVip";
32 | };
33 | EmailSettings = {
34 | SendEmailNotifications = config.EmailSettings.SendEmailNotifications;
35 | ConnectionSecurity = "${config.EmailSettings.ConnectionSecurity}";
36 | EnableSMTPAuth = config.EmailSettings.EnableSMTPAuth;
37 | SMTPPassword = "${config.EmailSettings.SMTPPassword}";
38 | SMTPPort = "${toString config.EmailSettings.SMTPPort}";
39 | SMTPServer = "${config.EmailSettings.SMTPServer}";
40 | SMTPUsername = "${config.EmailSettings.SMTPUsername}";
41 | RequireEmailVerification = config.EmailSettings.RequireEmailVerification;
42 | # SECURITY/FIXME: hardcoded
43 | InviteSalt = "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9YoS";
44 | # SECURITY/FIXME: hardcoded
45 | PasswordResetSalt = "vZ4DcKyVVRlKHHJpexcuXzojkE5PZ5eL";
46 | FeedbackName = "${config.EmailSettings.FeedbackName}";
47 | FeedbackEmail = "${config.EmailSettings.FeedbackEmail}";
48 | };
49 | };
50 |
51 | # Merge module options with the upstream config file and validate it.
52 | cfgFile = pkgs.runCommand "mattermost-config.json" rec {
53 | nativeBuildInputs = [ pkgs.mattermost pkgs.jq ];
54 | defaultConfig = "${pkgs.mattermost}/config/config.json";
55 | moduleConfig = builtins.toJSON config.config;
56 | inherit mainBinary;
57 | } ''
58 | echo -n "$moduleConfig" | jq -s '.[0] * .[1]' "$defaultConfig" - > "$out"
59 |
60 | # Mattermost version 4.x segfaults if it can't find its prefix in $PWD.
61 | ${lib.optionalString is5orNewer "cd ${lib.escapeShellArg pkgs.mattermost}"}
62 |
63 | if output="$("$mainBinary" config validate -c "$out" 2>&1)"; then
64 | echo "=========== mattermost syntax check ============"
65 | echo "syntax is ok! YAY! \o/"
66 | echo " -> $out"
67 | echo "=========== /mattermost syntax check ==========="
68 | else
69 | echo "=========== mattermost syntax check fail ==========="
70 | echo -e "$output"
71 | echo " -> $out"
72 | echo "=========== /mattermost syntax check fail ==========="
73 | echo "You need to fix your mattermost configuration!!1!"
74 | exit 1
75 | fi
76 | '';
77 |
78 | in {
79 | options = {
80 | siteName = mkOption {
81 | default = "Mattermost";
82 | description = "Name of the Mattermost instance";
83 | };
84 | config = mkOption {
85 | type = types.attrs;
86 | description = ''
87 | Configuration options for Mattermost as Nix attribute set in
88 | config.json schema. The documentation for these options can be found at
89 | .
90 | '';
91 | };
92 | EmailSettings = {
93 | SendEmailNotifications = mkOption {
94 | type = types.bool;
95 | default = false;
96 | description = ''
97 | Email notifications are used to inform offline users of chat activity.
98 | '';
99 | };
100 | FeedbackEmail = mkOption {
101 | type = types.str;
102 | default = "";
103 | description = ''
104 | Address displayed on email account used when sending notification emails from Mattermost system.
105 | '';
106 | };
107 | FeedbackName = mkOption {
108 | type = types.str;
109 | default = "";
110 | description = ''
111 | Address displayed on email account used when sending notification emails from Mattermost system.
112 | '';
113 | };
114 |
115 | ConnectionSecurity = mkOption {
116 | type = types.str;
117 | default = "STARTTLS";
118 | description = ''
119 | 'TLS' or 'STARTTLS' can be used to connect to the server.
120 | '';
121 | };
122 | EnableSMTPAuth = mkOption {
123 | type = types.bool;
124 | default = false;
125 | description = ''
126 | If you sent emails using localhost you won't need authentification, in all other cases you probably will!
127 | '';
128 | };
129 | SMTPPassword = mkOption {
130 | type = types.str;
131 | default = "";
132 | description = ''
133 | The password for your email account using SMTP.
134 | '';
135 | };
136 | SMTPPort = mkOption {
137 | type = types.int;
138 | default = 587;
139 | description = ''
140 | The port to connect in order to send emails. Usually 25 or 587.
141 | '';
142 | };
143 | SMTPServer = mkOption {
144 | type = types.str;
145 | default = "";
146 | example = "mail.example.org";
147 | description = ''
148 | Mail host to connect to.
149 | '';
150 | };
151 | SMTPUsername = mkOption {
152 | type = types.str;
153 | default = "";
154 | description = ''
155 | The user name to connect to. Often the email address but sometimes just the part before the @ like 'info' for info@nixcloud.io for instance.
156 | '';
157 | };
158 | RequireEmailVerification = mkOption {
159 | type = types.bool;
160 | default = false;
161 | description = ''
162 | RequireEmailVerification forces opt-in via email.
163 | '';
164 | };
165 | };
166 | };
167 |
168 | meta = {
169 | description = "A hybrid cloud enterprise messaging workspace";
170 | homepage = "https://mattermost.com/";
171 | license = lib.licenses.mit;
172 | maintainers = with lib.maintainers; [ qknight ];
173 | meta.platforms = lib.platforms.linux;
174 | };
175 |
176 | config = lib.mkIf config.enable {
177 | config = mattermostConfig;
178 |
179 | proxyOptions.websockets = {
180 | ws = {
181 | subpath = "/api/v4/websocket";
182 | };
183 | };
184 |
185 | users.mattermost = {
186 | description = "Mattermost server user";
187 | home = "${config.stateDir}/www";
188 | createHome = true;
189 | group = "mattermost";
190 | };
191 | groups.mattermost = {};
192 |
193 | database.mattermost = {
194 | user = "mattermost";
195 | owners = [ "mattermost" ];
196 | type = "postgresql";
197 | };
198 |
199 | systemd.services.mattermost =
200 | assert (config.proxyOptions.path == "/") || abort "Mattermost has no support for subdirectories yet, see 'https://mattermost.uservoice.com/forums/306457-general/suggestions/12468372-install-mattermost-in-a-subdirectory'. The path was ${config.proxyOptions.path}";
201 | {
202 | description = "${config.uniqueName} main service (mattermost)";
203 |
204 | wantedBy = [ "multi-user.target" ];
205 | after = [ "network.target" ];
206 |
207 | preStart = ''
208 | mkdir -p ${config.stateDir}/www/{data,config,logs}
209 | ln -sf ${pkgs.mattermost}/{bin,fonts,i18n,templates,client} ${config.stateDir}/www
210 | ln -sf ${cfgFile} ${config.stateDir}/www/config/config.json
211 | '';
212 |
213 | serviceConfig = {
214 | User = "mattermost";
215 | Group = "mattermost";
216 | Restart = "on-failure";
217 | WorkingDirectory = "${config.stateDir}/www";
218 | PrivateTmp = true;
219 | ExecStart = "${pkgs.mattermost}/bin/${mainBinary}";
220 | LimitNOFILE = "49152";
221 | };
222 | };
223 |
224 | tests.wanted = [ ./test.nix ];
225 | };
226 | }
227 |
--------------------------------------------------------------------------------
/modules/web/services/mattermost/test.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "mattermost";
3 |
4 | machine.nixcloud.reverse-proxy.enable = true;
5 | machine.nixcloud.reverse-proxy.extendEtcHosts = true;
6 | machine.nixcloud.webservices.mattermost.foo = {
7 | enable = true;
8 | proxyOptions = {
9 | domain = "example.com";
10 | http.mode = "on";
11 | https.mode = "off";
12 | port = 8080;
13 | };
14 | };
15 |
16 | testScript = let
17 | searchFor = "Mattermost";
18 | in ''
19 | $machine->waitForUnit('multi-user.target');
20 | # wait for reverse-proxy
21 | $machine->waitForOpenPort(80);
22 | # wait for the mattermost ELF (GO) binary
23 | $machine->waitForOpenPort(8080);
24 | $machine->succeed('curl -L http://example.com/ | grep -qF "${searchFor}"');
25 | '';
26 | }
27 |
--------------------------------------------------------------------------------
/modules/web/services/mediawiki/test.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "mediawiki";
3 |
4 | machine.nixcloud.reverse-proxy.enable = true;
5 | machine.nixcloud.reverse-proxy.extendEtcHosts = true;
6 | machine.nixcloud.webservices.mediawiki = {
7 | foo.enable = true;
8 | foo.defaultDatabaseType = "postgresql";
9 | foo.proxyOptions.domain = "example.com";
10 | foo.proxyOptions.http.mode = "on";
11 | foo.proxyOptions.https.mode = "off";
12 | foo.proxyOptions.port = 8080;
13 |
14 | bar.enable = true;
15 | bar.defaultDatabaseType = "mysql";
16 | bar.proxyOptions.domain = "example.org";
17 | bar.proxyOptions.http.mode = "on";
18 | bar.proxyOptions.https.mode = "off";
19 | bar.proxyOptions.port = 8081;
20 | };
21 |
22 | testScript = let
23 | searchFor = "Main Page - MediaWiki";
24 | in ''
25 | $machine->waitForUnit('multi-user.target');
26 | $machine->waitForOpenPort(80);
27 | $machine->waitForOpenPort(8080);
28 | $machine->waitForOpenPort(8081);
29 | $machine->succeed('curl -L http://example.com/ | grep -qF "${searchFor}"');
30 | $machine->succeed('curl -L http://example.org/ | grep -qF "${searchFor}"');
31 | '';
32 | }
33 |
--------------------------------------------------------------------------------
/modules/web/services/nginx/default.nix:
--------------------------------------------------------------------------------
1 | { config, options, lib, mkUniqueUser, mkUniqueGroup, ... }:
2 | {
3 | config = lib.mkMerge [
4 | {
5 | directories.www = {
6 | owner = mkUniqueUser config.webserver.user;
7 | group = mkUniqueGroup config.webserver.group;
8 | instance.before = [ "webserver-init.service" "instance-init.target" ];
9 | postCreate = ''
10 | cat > index.html <
12 |
13 |
14 |
15 | Nothing here yet?
16 |
17 |
18 | Nothing here yet?
19 | You can place files into
20 | ${config.stateDir}/www
.
21 |
22 |
23 | EOF
24 | '';
25 | };
26 | }
27 | {
28 | webserver.variant = "nginx";
29 | tests.wanted = [ ./test.nix ];
30 | }
31 | ];
32 | meta = {
33 | description = "Declarative nginx backend implementation for hacking";
34 | maintainers = with lib.maintainers; [ qknight ];
35 | license = lib.licenses.bsd2;
36 | homepage = https://github.com/nixcloud/nixcloud-webservices;
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/modules/web/services/nginx/test.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "nginx";
3 | machine.nixcloud.reverse-proxy.enable = true;
4 | machine.nixcloud.reverse-proxy.extendEtcHosts = true;
5 | machine.nixcloud.webservices.nginx = {
6 | foo = {
7 | enable = true;
8 | proxyOptions = {
9 | domain = "example.com";
10 | http.mode = "on";
11 | https.mode = "off";
12 | port = 8080;
13 | };
14 | webserver.nginx = {
15 | extraConfig = ''
16 | location / {
17 | root /var/lib/nixcloud/webservices/nginx-foo/www/;
18 | }
19 | '';
20 | };
21 | };
22 | };
23 |
24 | testScript = ''
25 | $machine->waitForUnit('multi-user.target');
26 | $machine->waitForOpenPort(80);
27 | $machine->succeed('curl http://example.com | grep -qF "Nothing here yet"');
28 | '';
29 | }
30 |
--------------------------------------------------------------------------------
/modules/web/services/roundcube/default.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, apache, mkUniqueUser, mkUniqueGroup, ... }:
2 |
3 | with lib;
4 | let
5 | roundcubeConfigFile = pkgs.writeText "config.inc.php" ''
6 | 'sqlite:///${config.stateDir}/roundcube/sqlite.db?mode=0600',
9 | 'log_dir' => '${config.stateDir}/log',
10 | 'enable_spellcheck' => ${if config.config.enable_spellcheck then "True" else "False"},
11 | );
12 | ${config.extraConfig}
13 | '';
14 | in
15 | {
16 | options = {
17 | config = {
18 | enable_spellcheck = mkOption {
19 | type = types.bool;
20 | default = false;
21 | description = ''
22 | Enable spellchecking when composing mails
23 | WARNING: Due to possible privacy implications when using an online spellchecking
24 | service this function is disabled by default.
25 | '';
26 | };
27 | spellcheck_engine = mkOption {
28 | type = types.nullOr (types.enum ["googie" "pspell" "enchant" "atd"]);
29 | default = null;
30 | description = ''
31 | The spellcheck engine to be used.
32 | WARNING: Some engines might use online services, which has privacy implications.
33 | '';
34 | };
35 | };
36 | extraConfig = mkOption {
37 | type = types.lines;
38 | default = "";
39 | example = ''
40 | $config['log_date_format'] = 'd-M-Y H:i:s O';
41 | $config['imap_timeout'] = 30;
42 | '';
43 | description = ''
44 | Any additional lines to be appended to Roundcube's
45 | configuration file.
46 | See the reference documentation:
47 | .
48 | '';
49 | };
50 | };
51 |
52 | config = let
53 | version = "1.3.9";
54 | roundcubeRoot = pkgs.stdenv.mkDerivation rec {
55 | name= "roundcube-${version}";
56 |
57 | src = pkgs.fetchurl {
58 | url = "https://github.com/roundcube/roundcubemail/releases/download/${version}/roundcubemail-${version}-complete.tar.gz";
59 | sha256 = "726db4ffb33a7154dd432cbb99810ab9d02512c7f1987a6119e9ac7f595521ad";
60 | };
61 |
62 | buildPhase = ''
63 | rm -rf installer
64 | '';
65 | installPhase = ''
66 | mkdir -p $out
67 | cp -r . $out
68 | ln -sf ${roundcubeConfigFile} $out/config/config.inc.php
69 | '';
70 | };
71 | in {
72 | webserver.variant = "apache";
73 | webserver.systemPackages = [pkgs.php];
74 | webserver.init = ''
75 | mkdir -p ${config.stateDir}/roundcube/log
76 | chown -R ${mkUniqueUser config.webserver.user}:${mkUniqueGroup config.webserver.group} ${config.stateDir}/roundcube
77 | chmod -R 750 ${config.stateDir}/roundcube
78 | '';
79 | webserver.apache.enablePHP = true;
80 | webserver.apache.extraConfig = ''
81 |
82 | Require all granted
83 |
84 |
85 | Require all denied
86 |
87 | DocumentRoot ${roundcubeRoot}
88 | DirectoryIndex index.php
89 | '';
90 | };
91 |
92 | meta = {
93 | description = "Open Source Webmail Software";
94 | maintainers = with maintainers; [ eliasp ];
95 | license = licenses.gpl3;
96 | platforms = platforms.all;
97 | };
98 | }
99 |
--------------------------------------------------------------------------------
/modules/web/services/static-darkhttpd/default.nix:
--------------------------------------------------------------------------------
1 | { config, lib, ... }:
2 |
3 | {
4 | options = {
5 | root = lib.mkOption {
6 | type = lib.types.path;
7 | default = "${config.stateDir}/www";
8 | example = /var/www/whatever;
9 | description = "The directory where the static webserver looks for documents to serve.";
10 | };
11 | };
12 |
13 | config = lib.mkMerge [
14 | (lib.mkIf (config.root == "${config.stateDir}/www") {
15 | # XXX: Make DRY with the one in static-nginx!
16 | directories.www.postCreate = ''
17 | cat > index.html <
19 |
20 |
21 |
22 | Nothing here yet?
23 |
24 |
25 | Nothing here yet?
26 | You can place files into
27 | ${config.stateDir}/www
.
28 |
29 |
30 | EOF
31 | '';
32 | })
33 | { webserver.variant = "darkhttpd";
34 | webserver.darkhttpd.root = config.root;
35 | tests.wanted = [ ./test.nix ];
36 | }
37 | ];
38 |
39 | meta = {
40 | description = "Using darkhttpd for static file serving (no CGI)";
41 | maintainers = with lib.maintainers; [ qknight ];
42 | license = lib.licenses.bsd2;
43 | homepage = https://github.com/nixcloud/nixcloud-webservices;
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/modules/web/services/static-darkhttpd/test.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "static-darkhttpd";
3 |
4 | machine.nixcloud.reverse-proxy.enable = true;
5 | machine.nixcloud.reverse-proxy.extendEtcHosts = true;
6 | machine.nixcloud.webservices.static-darkhttpd = {
7 | foo.enable = true;
8 | foo.proxyOptions.domain = "example.com";
9 | foo.proxyOptions.http.mode = "on";
10 | foo.proxyOptions.https.mode = "off";
11 | foo.proxyOptions.port = 8080;
12 |
13 | bar.enable = true;
14 | bar.proxyOptions.domain = "example.org";
15 | bar.proxyOptions.http.mode = "on";
16 | bar.proxyOptions.https.mode = "off";
17 | bar.proxyOptions.port = 8081;
18 | };
19 |
20 | testScript = ''
21 | $machine->waitForUnit('multi-user.target');
22 | $machine->waitForOpenPort(80);
23 | $machine->succeed(
24 | 'curl -L http://example.com/ | grep -q "Nothing here yet"',
25 | 'curl -L http://example.org/ | grep -q "Nothing here yet"'
26 | );
27 | '';
28 | }
29 |
--------------------------------------------------------------------------------
/modules/web/services/static-nginx/default.nix:
--------------------------------------------------------------------------------
1 | { config, lib, mkUniqueUser, mkUniqueGroup, ... }:
2 |
3 | {
4 | options = {
5 | root = lib.mkOption {
6 | type = lib.types.path;
7 | default = "${config.stateDir}/www";
8 | example = /var/www/whatever;
9 | description = "The directory where the static webserver looks for documents to serve.";
10 | };
11 | };
12 |
13 | config = lib.mkMerge [
14 | (lib.mkIf (config.root == "${config.stateDir}/www") {
15 | directories.www = {
16 | owner = mkUniqueUser config.webserver.user;
17 | group = mkUniqueGroup config.webserver.group;
18 | instance.before = [ "webserver-init.service" "instance-init.target" ];
19 | # XXX: Make DRY with the one in static-darkhttpd!
20 | postCreate = ''
21 | cat > index.html <
23 |
24 |
25 |
26 | Nothing here yet?
27 |
28 |
29 | Nothing here yet?
30 | You can place files into
31 | ${config.stateDir}/www
.
32 |
33 |
34 | EOF
35 | '';
36 | };
37 | })
38 | { webserver.variant = "nginx";
39 | webserver.nginx.extraConfig = ''
40 | index index.html;
41 | root ${toString config.root};
42 | '';
43 | tests.wanted = [ ./test.nix ];
44 | }
45 | ];
46 |
47 | meta = {
48 | description = "Using nginx for static file serving (no CGI)";
49 | maintainers = with lib.maintainers; [ qknight ];
50 | license = lib.licenses.bsd2;
51 | homepage = https://github.com/nixcloud/nixcloud-webservices;
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/modules/web/services/static-nginx/test.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "static-nginx";
3 |
4 | machine.nixcloud.reverse-proxy.enable = true;
5 | machine.nixcloud.reverse-proxy.extendEtcHosts = true;
6 | machine.nixcloud.webservices.static-nginx = {
7 | foo.enable = true;
8 | foo.proxyOptions.domain = "example.com";
9 | foo.proxyOptions.http.mode = "on";
10 | foo.proxyOptions.https.mode = "off";
11 | foo.proxyOptions.port = 8080;
12 |
13 | bar.enable = true;
14 | bar.proxyOptions.domain = "example.org";
15 | bar.proxyOptions.http.mode = "on";
16 | bar.proxyOptions.https.mode = "off";
17 | bar.proxyOptions.port = 8081;
18 | };
19 |
20 | testScript = ''
21 | $machine->waitForUnit('multi-user.target');
22 | $machine->waitForOpenPort(80);
23 | $machine->succeed(
24 | 'curl -L http://example.com/ | grep -q "Nothing here yet"',
25 | 'curl -L http://example.org/ | grep -q "Nothing here yet"'
26 | );
27 | '';
28 | }
29 |
--------------------------------------------------------------------------------
/modules/web/webserver/darkhttpd.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, options, wsName, mkUniqueUser, mkUniqueGroup, ... }:
2 |
3 | with lib;
4 |
5 | let
6 | enabled = config.webserver.variant == "darkhttpd" && config.enable;
7 | isDefaultRoot = config.webserver.darkhttpd.root == "${config.stateDir}/www";
8 | in {
9 | options.webserver.darkhttpd = {
10 | root = mkOption {
11 | type = types.path;
12 | default = "${config.stateDir}/www";
13 | example = /var/www/whatever;
14 | description = "The directory where the static webserver looks for documents to serve.";
15 | };
16 | extraServiceDependencies = mkOption {
17 | type = types.listOf types.str;
18 | default = [ ];
19 | example = [ "postgresql.service" ];
20 | description = "Makes it easy to replace postgresql by mysql and depend on the service before we start the webservice.";
21 | };
22 | };
23 |
24 | config = lib.mkMerge [
25 | (mkIf (enabled && isDefaultRoot) {
26 | directories.www = {
27 | owner = mkUniqueUser config.webserver.user;
28 | group = mkUniqueGroup config.webserver.group;
29 | instance.before = [ "webserver-init.service" "instance-init.target" ];
30 | };
31 | })
32 | (mkIf enabled {
33 | directories.log = {
34 | permissions.defaultDirectoryMode = "0750";
35 | permissions.others.noAccess = true;
36 | owner = mkUniqueUser config.webserver.user;
37 | group = mkUniqueGroup config.webserver.group;
38 | instance.before = [ "webserver-init.service" "instance-init.target" ];
39 | };
40 |
41 | systemd.services.darkhttpd = {
42 | description = "${config.uniqueName} main service (darkhttpd)";
43 | wantedBy = [ "multi-user.target" ];
44 | wants = [ "keys.target" ];
45 | after = [ "network.target" "fs.target" "keys.target" ];
46 | instance.after = [ "database.target" "webserver-init.service" ];
47 |
48 | serviceConfig = {
49 | ExecStart = "${pkgs.darkhttpd}/bin/darkhttpd ${toString config.root} --port ${toString config.proxyOptions.port} --mimetypes ${pkgs.apacheHttpd}/conf/mime.types --addr 127.0.0.1";
50 | KillSignal = "SIGTERM";
51 | Restart = "always";
52 | RestartSec = "10s";
53 | StartLimitInterval = "1min";
54 | User = config.webserver.user;
55 | Group = config.webserver.group;
56 | PermissionsStartOnly = true;
57 | PrivateTmp = config.webserver.privateTmp;
58 | WorkingDirectory = config.stateDir;
59 | MemoryDenyWriteExecute = true;
60 | RestrictNameSpaces = true;
61 | NoNewPrivileges = true;
62 | ProtectHome = true;
63 | PrivateUsers = true;
64 | ProtectSystem = true;
65 | ProtectKernelTunables = true;
66 | };
67 | };
68 | })
69 | ];
70 | }
71 |
--------------------------------------------------------------------------------
/modules/web/webserver/lib/apache_check_config.nix:
--------------------------------------------------------------------------------
1 | # nixcloud.io configuration checker function for apache
2 |
3 | { pkgs, lib }:
4 | with lib;
5 |
6 | let
7 | #awk -f /tmp/z.awk /tmp/apache.conf
8 | awkFormat = pkgs.writeText "awkFormat.awk" ''
9 | awk -f
10 | {sub(/^[ \t]+/,"");idx=0}
11 | /\{/{ctx++;idx=1}
12 | /\}/{ctx--}
13 | {id="";for(i=idx;i $out/${fileName}
26 | set +e
27 |
28 | echo "=========== apache config cleanup ==========="
29 |
30 | cp $out/${fileName} $out/${fileName}_
31 | echo "Refining apache configuration so it will pass tests even we don't have directories setup properly."
32 | echo " -> $out/${fileName}_ <- file for testing"
33 | echo " Note: during nix-build, with 'nix.useSandbox=true', there won't be any apache specific directories yet!"
34 | sed -i -E "s|DefaultRuntimeDir .*|DefaultRuntimeDir /tmp|g" $out/${fileName}_
35 | sed -i -E "s|ErrorLog .*||g" $out/${fileName}_
36 | sed -i -E "s|CustomLog .*||g" $out/${fileName}_
37 | sed -i -E "s|User .*||g" $out/${fileName}_
38 | sed -i -E "s|Group .*||g" $out/${fileName}_
39 | sed -i -E "s|DocumentRoot .*|DocumentRoot /tmp|g" $out/${fileName}_
40 |
41 | echo "=========== /apache config cleanup ==========="
42 |
43 | t=$(${pkgs.apacheHttpd}/bin/httpd -t -f $out/${fileName}_ 2>&1)
44 |
45 | echo $t | grep 'Syntax OK' > /dev/null
46 | status=$?
47 | set -e
48 |
49 | if [ "$status" != "0" ]; then
50 | echo "=========== apache syntax check fail ==========="
51 | echo -e "$t"
52 | echo " -> $out/${fileName}_ <- file for testing"
53 | echo " -> $out/${fileName}"
54 | echo "=========== /apache syntax check fail ==========="
55 | echo "You need to fix your apache configuration!!1!"
56 | exit 1
57 | else
58 | echo "=========== apache syntax check ==========="
59 | echo "syntax is ok! YAY! \o/"
60 | echo " -> $out/${fileName}"
61 | echo " -> status: $status"
62 | echo "=========== /apache syntax check ==========="
63 | rm $out/${fileName}_
64 | fi
65 | '';
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/modules/web/webserver/lib/nginx_check_config.nix:
--------------------------------------------------------------------------------
1 | # nixcloud.io configuration checker function for nginx
2 |
3 | { pkgs, lib }:
4 | with lib;
5 |
6 | let
7 | #awk -f /tmp/z.awk /nix/store/nginx.conf
8 | awkFormat = pkgs.writeText "awkFormat.awk" ''
9 | awk -f
10 | {sub(/^[ \t]+/,"");idx=0}
11 | /\{/{ctx++;idx=1}
12 | /\}/{ctx--}
13 | {id="";for(i=idx;i $out/${fileName}
26 |
27 | # this checks if the config is sane
28 | touch nginx.pid
29 |
30 | # nginx -t requires certificates to be there! we don't have certificates, even snaikoil ones which are created by ACME, before service startup!
31 | # so this smart code, borrowed from fpletz, will create one
32 | echo "------------ generating fake certificates for nginx syntax check ---------------"
33 | workdir=./
34 | ${pkgs.openssl.bin}/bin/openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048
35 | ${pkgs.openssl.bin}/bin/openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key
36 | ${pkgs.openssl.bin}/bin/openssl req -new -key $workdir/server.key -out $workdir/server.csr \
37 | -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
38 | ${pkgs.openssl.bin}/bin/openssl x509 -req -days 1 -in $workdir/server.csr -signkey $workdir/server.key -out $workdir/server.crt
39 |
40 | # Move key to destination
41 | mv $workdir/server.key nginx_generated_testing_key.pem
42 | mv $workdir/server.crt nginx_generated_testing_fullchain.pem
43 |
44 | cp *.key *.pem $out/
45 |
46 | cp $out/${fileName} $out/${fileName}_
47 | sed -i -E "s|ssl_certificate .*;|ssl_certificate ./nginx_generated_testing_fullchain.pem;|g" $out/${fileName}_
48 | sed -i -E "s|ssl_certificate_key.*;|ssl_certificate_key ./nginx_generated_testing_key.pem;|g" $out/${fileName}_
49 | echo "------------ /generating fake certificates for nginx syntax check ---------------"
50 |
51 | # using 'set +e' here since nginx not only checks the syntax but executes the webserver which always fails
52 | # as the environment isn't and can't be setup propperly at this stage.
53 | # checking the syntax is already a huge gain as we see these errors during nix-evaluation and don't have to
54 | # deploy with a broken configuration file in order to see it 'fail'.
55 | set +e
56 | t=$(${pkgs.nginx}/bin/nginx -t -c $out/${fileName}_ -g "pid nginx.pid; worker_processes 2; " 2>&1)
57 | echo $t | grep 'syntax is ok'
58 | status=$?
59 | set -e
60 |
61 | if [ "$status" != "0" ]; then
62 | echo "=========== nginx syntax check fail ==========="
63 | echo -e "$t"
64 | echo " -> $out/${fileName}"
65 | echo "=========== /nginx syntax check fail ==========="
66 | echo "You need to fix your nginx configuration!!1!"
67 | exit 1
68 | else
69 | echo "=========== nginx syntax check ==========="
70 | echo "syntax is ok! YAY! \o/"
71 | echo " -> $out/${fileName}"
72 | echo "=========== /nginx syntax check ==========="
73 | rm $out/${fileName}_
74 | rm $out/nginx_generated_testing_key.pem
75 | rm $out/nginx_generated_testing_fullchain.pem
76 | rm $out/server.pass.key
77 | fi
78 | '';
79 | };
80 | }
81 |
--------------------------------------------------------------------------------
/modules/web/webserver/nginx.nix:
--------------------------------------------------------------------------------
1 | { config, pkgs, lib, options, wsName, mkUniqueUser, mkUniqueGroup, ... }:
2 |
3 | with lib;
4 |
5 | {
6 | options.webserver.nginx = {
7 | # FIXME: add package as in apache
8 | extraConfig = mkOption {
9 | type = types.lines;
10 | default = "";
11 | description = ''
12 | Cnfiguration lines appended to the generated Nginx
13 | configuration file.
14 | '';
15 | };
16 | extraServiceDependencies = mkOption {
17 | type = types.listOf types.str;
18 | default = [ ];
19 | example = [ "postgresql.service" ];
20 | description = "Makes it easy to replace postgresql by mysql and depend on the service before we start the webservice.";
21 | };
22 | };
23 |
24 | config = let
25 | fastcgi_params = pkgs.writeText "fastcgi_params.conf" ''
26 | fastcgi_param QUERY_STRING $query_string;
27 | fastcgi_param REQUEST_METHOD $request_method;
28 | fastcgi_param CONTENT_TYPE $content_type;
29 | fastcgi_param CONTENT_LENGTH $content_length;
30 |
31 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
32 | fastcgi_param SCRIPT_NAME $fastcgi_script_name;
33 | fastcgi_param PATH_INFO $fastcgi_path_info;
34 | fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
35 | fastcgi_param REQUEST_URI $request_uri;
36 | fastcgi_param DOCUMENT_URI $document_uri;
37 | fastcgi_param DOCUMENT_ROOT $document_root;
38 | fastcgi_param SERVER_PROTOCOL $server_protocol;
39 |
40 | fastcgi_param GATEWAY_INTERFACE CGI/1.1;
41 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
42 |
43 | fastcgi_param REMOTE_ADDR $remote_addr;
44 | fastcgi_param REMOTE_PORT $remote_port;
45 | fastcgi_param SERVER_ADDR $server_addr;
46 | fastcgi_param SERVER_PORT $server_port;
47 | fastcgi_param SERVER_NAME $server_name;
48 |
49 | #fastcgi_param HTTPS $https;
50 |
51 | # PHP only, required if PHP was built with --enable-force-cgi-redirect
52 | fastcgi_param REDIRECT_STATUS 200;
53 | '';
54 | in mkIf (config.webserver.variant == "nginx" && config.enable) {
55 |
56 | directories = lib.genAttrs [ "nginx" "nginx/logs" ] (lib.const {
57 | owner = mkUniqueUser config.webserver.user;
58 | group = mkUniqueGroup config.webserver.group;
59 | instance.before = [ "webserver-init.service" "instance-init.target" ];
60 | });
61 |
62 | systemd.services.nginx = let
63 | fileName = "${config.uniqueName}.conf";
64 | in {
65 | description = "Nginx HTTPD";
66 | wantedBy = [ "multi-user.target" ];
67 | after = [ "network.target" "fs.target" "keys.target" ] ++ config.webserver.nginx.extraServiceDependencies;
68 | instance.after = [ "database.target" "webserver-init.service" ];
69 | serviceConfig = let
70 | checkAndFormatNginxConfigfile = (import lib/nginx_check_config.nix {inherit lib pkgs;}).checkAndFormatNginxConfigfile {configFile = nginxConfigFile; inherit fileName;};
71 |
72 | # FIXME: add user record only if run as root (which is not the case if PermissionsStartOnly=false IIRC)
73 | nginxConfigFile = pkgs.writeText "${config.uniqueName}.conf" ''
74 | user "${mkUniqueUser config.webserver.user}" "${mkUniqueGroup config.webserver.group}";
75 | error_log stderr;
76 | daemon off;
77 | events {}
78 |
79 | http {
80 | server {
81 | #listen unix:/var/run/nginx.sock;
82 | listen ${toString config.proxyOptions.port};
83 | access_log ${config.stateDir}/nginx/logs/access.log;
84 | error_log ${config.stateDir}/nginx/logs/error.log;
85 | server_name "${config.proxyOptions.domain}";
86 |
87 | ${config.webserver.nginx.extraConfig}
88 | }
89 | }
90 | '';
91 |
92 | in {
93 | ExecStart = "${pkgs.nginx}/bin/nginx -c ${checkAndFormatNginxConfigfile}/${fileName} -p ${config.stateDir}/nginx";
94 | ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
95 | PIDFile = "${config.runtimeDir}/nginx.pid";
96 |
97 | Restart = "always";
98 | RestartSec = "10s";
99 | StartLimitInterval = "1min";
100 | User = config.webserver.user;
101 | Group = config.webserver.group;
102 | PermissionsStartOnly=true;
103 | PrivateTmp=config.webserver.privateTmp;
104 | };
105 | };
106 | };
107 | }
108 |
--------------------------------------------------------------------------------
/pkgs/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import {} }:
2 |
3 | let
4 | self = {
5 | callPackage = pkgs.newScope self;
6 |
7 | nixcloud = import ./nixcloud {
8 | inherit (self) callPackage;
9 | inherit pkgs;
10 | };
11 | };
12 |
13 | in self
14 |
--------------------------------------------------------------------------------
/pkgs/nixcloud/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs, ... }:
2 | {
3 | # one can add extra packags here like:
4 | #lego = pkgs.callPackage ./lego/default.nix {};
5 | # one can use them with pkgs.nixcloud.lego instead of pkgs.lego for instance
6 | container = pkgs.stdenv.mkDerivation rec {
7 | name = "nixcloud-container-${version}";
8 | version = "0.0.1";
9 |
10 | src = pkgs.fetchFromGitHub {
11 | owner = "nixcloud";
12 | repo = "nixcloud-container";
13 | rev = "fd95f6b53a44dc76b6cbbed5c0299b9b900ed72c";
14 | sha256 = "1m76w57qbymk3n0ws850fcplgws2r4140ahilh6harrf9irv70j5";
15 | };
16 |
17 | nativeBuildInputs = [ pkgs.makeWrapper ];
18 |
19 | installPhase = ''
20 | mkdir -p $out/bin
21 | cp -r $src/bin/* $out/bin
22 | mkdir -p $out/share
23 | cp -r $src/test.nix $out/share
24 | # FIXME/HACK nixos-container should probably be run from a 'interactive shell' which already contains a valid
25 | # NIX_PATH set (note: the NIX_PATH currently set points to a none-existing path, WTH?)
26 | # or
27 | # we should abstract over 'nix-channel' and for instance also push updates when they are avialbel ASAP
28 | wrapProgram $out/bin/nixcloud-container \
29 | --prefix PATH : "${pkgs.stdenv.lib.makeBinPath [ pkgs.lxc pkgs.nix pkgs.eject ]}" # eject because of util-linux and flock
30 | '';
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/release.nix:
--------------------------------------------------------------------------------
1 | { system ? builtins.currentSystem, nixpkgs ?
2 |
3 | # This is a self-reference, so be sure to call the Hydra jobset input
4 | # 'nixcloud-webservices', otherwise the version will be wrong.
5 | , nixcloud-webservices ? { outPath = ./.; revCount = 1234; rev = "abcdef"; }
6 | }:
7 |
8 | let
9 | pkgs = import nixpkgs { inherit system; };
10 | inherit (pkgs) lib;
11 |
12 | tests = let
13 | mkJob = lib.const lib.hydraJob;
14 | mapHydra = lib.mapAttrsRecursiveCond (t: !(t ? test)) mkJob;
15 | in mapHydra (import ./tests {
16 | inherit system nixpkgs;
17 | pkgs = import nixpkgs {};
18 | });
19 |
20 | manual = let
21 | modules = import "${nixpkgs}/nixos/lib/eval-config.nix" {
22 | modules = [ ./modules ];
23 | check = false;
24 | inherit system;
25 | };
26 |
27 | isNcOpt = opt: lib.head (lib.splitString "." opt.name) == "nixcloud";
28 | filterDoc = lib.filter (opt: isNcOpt opt && opt.visible && !opt.internal);
29 | filtered = filterDoc (lib.optionAttrSetToDocList modules.options);
30 | # XXX: Instead of unsafe discard, let's actually strip the prefix someday.
31 | optionsXML = builtins.unsafeDiscardStringContext (builtins.toXML filtered);
32 | optionsFile = builtins.toFile "options.xml" optionsXML;
33 |
34 | in pkgs.stdenv.mkDerivation {
35 | name = "nixcloud-webservices-options";
36 |
37 | nativeBuildInputs = [ pkgs.libxslt ];
38 |
39 | styleSheets = [
40 | "style.css" "overrides.css" "highlightjs/mono-blue.css"
41 | ];
42 |
43 | buildCommand = ''
44 | dest="$out/share/doc/nixcloud-webservices"
45 | mkdir -p "$dest"
46 |
47 | cat > manual.xml <
51 | NixOS options specific to nixcloud webservices
52 |
53 |
54 | XML
55 |
56 | xsltproc -o intermediate.xml \
57 | "${pkgs.path}/nixos/doc/manual/options-to-docbook.xsl" \
58 | ${lib.escapeShellArg optionsFile}
59 |
60 | xsltproc -o options-db.xml \
61 | "${pkgs.path}/nixos/doc/manual/postprocess-option-descriptions.xsl" \
62 | intermediate.xml
63 |
64 | xsltproc -o "$dest/index.html" -nonet -xinclude \
65 | --param section.autolabel 1 \
66 | --param section.label.includes.component.label 1 \
67 | --stringparam html.stylesheet \
68 | 'style.css overrides.css highlightjs/mono-blue.css' \
69 | --stringparam html.script \
70 | 'highlightjs/highlight.pack.js highlightjs/loader.js' \
71 | --param xref.with.number.and.title 1 \
72 | --stringparam admon.style "" \
73 | ${pkgs.docbook5_xsl}/xml/xsl/docbook/xhtml/docbook.xsl \
74 | manual.xml
75 |
76 | cp "${nixpkgs}/doc/style.css" "$dest/style.css"
77 | cp "${nixpkgs}/doc/overrides.css" "$dest/overrides.css"
78 | cp -r ${pkgs.documentation-highlighter} "$dest/highlightjs"
79 |
80 | mkdir -p "$out/nix-support"
81 | echo "doc manual $dest" > "$out/nix-support/hydra-build-products"
82 | '';
83 | };
84 |
85 | # All the jobs excluding the channel, because they will be constituents for
86 | # the channel.
87 | jobs = {
88 | inherit tests manual;
89 | };
90 |
91 | in jobs // {
92 | # This is the channel and it is called 'nixcloud-webservices' to ensure that
93 | # when the channel is added without an explicit name argument, it will be
94 | # available as .
95 | nixcloud-webservices = pkgs.releaseTools.channel {
96 | name = "nixcloud-webservices";
97 | constituents = lib.collect lib.isDerivation jobs;
98 | src = nixcloud-webservices;
99 | patchPhase = ''
100 | touch .update-on-nixos-rebuild
101 | cat > default.nix < /dev/null
32 |
33 | printAvailable() {
34 | echo "Available web service names are:" >&2
35 | echo >&2
36 | eval "$(nix-instantiate --eval --strict --json -E '
37 | builtins.attrNames (import ./run-webservice.nix {})
38 | ' | jq -r '@sh "for i in \(.); do echo \" * $i\" >&2; done"')"
39 | echo >&2
40 | }
41 |
42 | printUsage() {
43 | cat >&2 <&2
117 | printUsage
118 | exit 1
119 | fi
120 |
121 | result="$(nix-build --no-out-link --arg wsConfig "$wsConfig" \
122 | run-webservice.nix -A "$wsName" "${nixBuildOptions[@]}")"
123 |
124 | popd > /dev/null
125 | exec "$result" "$@"
126 |
--------------------------------------------------------------------------------
/tests/common/eatmydata.nix:
--------------------------------------------------------------------------------
1 | { config, lib, pkgs, ... }:
2 |
3 | let
4 | perWS = lib.const (cfg: let
5 | enabledDbs = lib.optionalAttrs cfg.enable cfg.database;
6 | dbs = lib.mapAttrsToList (lib.const (db: db.type)) enabledDbs;
7 | value.environment.LD_PRELOAD = "${pkgs.libeatmydata}/lib/libeatmydata.so";
8 | in lib.optional (lib.elem "mysql" dbs) {
9 | name = "${cfg.uniqueName}-mysql";
10 | inherit value;
11 | } ++ lib.optional (lib.elem "postgresql" dbs) {
12 | name = "${cfg.uniqueName}-postgresql";
13 | inherit value;
14 | });
15 |
16 | perWSName = lib.const (cfg: lib.concatLists (lib.mapAttrsToList perWS cfg));
17 | serviceList = lib.mapAttrsToList perWSName config.nixcloud.webservices;
18 |
19 | in {
20 | systemd.services = lib.listToAttrs (lib.concatLists serviceList);
21 | }
22 |
--------------------------------------------------------------------------------
/tests/custom-webservice.nix:
--------------------------------------------------------------------------------
1 | let
2 | customWebService = let
3 | submodule = { pkgs, ... }: {
4 | config.webserver.variant = "lighttpd";
5 | config.webserver.lighttpd.extraConfig = ''
6 | server.document-root = "${pkgs.runCommand "docroot" {} ''
7 | mkdir "$out"
8 | echo hello world > "$out/index.html"
9 | ''}"
10 | '';
11 | meta.license = null;
12 | };
13 | in import ../lib/make-webservice.nix "custom-foobar" submodule;
14 |
15 | in {
16 | name = "custom-webservice";
17 |
18 | machine = { lib, ... }: {
19 | imports = lib.singleton customWebService;
20 |
21 | nixcloud.reverse-proxy.enable = true;
22 | nixcloud.reverse-proxy.extendEtcHosts = true;
23 |
24 | nixcloud.webservices.custom-foobar = {
25 | foo.enable = true;
26 | foo.proxyOptions.domain = "example.com";
27 | foo.proxyOptions.http.mode = "on";
28 | foo.proxyOptions.https.mode = "off";
29 | foo.proxyOptions.port = 8080;
30 |
31 | bar.enable = true;
32 | bar.proxyOptions.domain = "example.org";
33 | bar.proxyOptions.http.mode = "on";
34 | bar.proxyOptions.https.mode = "off";
35 | bar.proxyOptions.port = 8081;
36 | };
37 | };
38 |
39 | testScript = ''
40 | $machine->waitForUnit('multi-user.target');
41 | $machine->waitForOpenPort(80);
42 | $machine->succeed('curl http://example.com/ | grep -qF "hello world"');
43 | $machine->succeed('curl http://example.org/ | grep -qF "hello world"');
44 | '';
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/tests/dbshell.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "dbshell";
3 |
4 | machine = { pkgs, ... }: {
5 | environment.systemPackages = [ pkgs.mariadb ];
6 | nixcloud.reverse-proxy.enable = true;
7 | nixcloud.reverse-proxy.extendEtcHosts = true;
8 | nixcloud.webservices.mediawiki = {
9 | foo.enable = true;
10 | foo.defaultDatabaseType = "postgresql";
11 | foo.proxyOptions.domain = "example.com";
12 | foo.proxyOptions.http.mode = "on";
13 | foo.proxyOptions.https.mode = "off";
14 | foo.proxyOptions.port = 8080;
15 |
16 | bar.enable = true;
17 | bar.defaultDatabaseType = "mysql";
18 | bar.proxyOptions.domain = "example.org";
19 | bar.proxyOptions.http.mode = "on";
20 | bar.proxyOptions.https.mode = "off";
21 | bar.proxyOptions.port = 8081;
22 | };
23 | };
24 |
25 | testScript = ''
26 | $machine->waitForUnit('multi-user.target');
27 |
28 | subtest "shell works with explicit web service name", sub {
29 | $machine->succeed(
30 | 'echo "\\dt" | nixcloud-dbshell mediawiki-foo mediawiki'.
31 | ' | grep -q interwiki'
32 | );
33 |
34 | $machine->succeed(
35 | 'echo SHOW TABLES | nixcloud-dbshell mediawiki-bar mediawiki'.
36 | ' | grep -q interwiki'
37 | );
38 | };
39 |
40 | subtest "shell works without explicit web service name", sub {
41 | $machine->succeed(
42 | 'cd /var/lib/nixcloud/webservices/mediawiki-bar/mysql'.
43 | ' && echo SHOW TABLES | nixcloud-dbshell mediawiki'.
44 | ' | grep -q interwiki'
45 | );
46 | };
47 | '';
48 | }
49 |
--------------------------------------------------------------------------------
/tests/default.nix:
--------------------------------------------------------------------------------
1 | { system ? builtins.currentSystem
2 | , pkgs ? import { inherit system; }
3 | , nixpkgs ? pkgs.path
4 | }:
5 |
6 | let
7 | callTest = import ../lib/call-test.nix { inherit system pkgs nixpkgs; };
8 | in {
9 | custom-webservice = callTest ./custom-webservice.nix;
10 | dbshell = callTest ./dbshell.nix;
11 | directories = callTest ./directories.nix;
12 | ip2unix = callTest ./ip2unix.nix;
13 | mkunique = callTest ./mkunique.nix;
14 | user-allocation-uid-gid-test = callTest ./user-allocation-uid-gid-test.nix;
15 | version = callTest ./version.nix;
16 | container = callTest ./container.nix;
17 | TLS = callTest ../modules/services/TLS/test.nix;
18 |
19 | # XXX: These tests should be automatically gathered by the module system!
20 | reverse-proxy = callTest ../modules/services/reverse-proxy/test.nix;
21 | email = callTest ../modules/services/email/test;
22 | messaging.rabbitmq = callTest ../modules/web/messaging/rabbitmq/test.nix;
23 | webservices.mediawiki = callTest ../modules/web/services/mediawiki/test.nix;
24 | webservices.mattermost = callTest ../modules/web/services/mattermost/test.nix;
25 | webservices.hydra = callTest ../modules/web/services/hydra/test.nix;
26 | webservices.leaps = callTest ../modules/web/services/leaps/test.nix;
27 | webservices.static-darkhttpd = callTest
28 | ../modules/web/services/static-darkhttpd/test.nix;
29 | webservices.static-nginx = callTest
30 | ../modules/web/services/static-nginx/test.nix;
31 | webservices.apache = callTest ../modules/web/services/apache/test.nix;
32 | webservices.nginx = callTest ../modules/web/services/nginx/test.nix;
33 | }
34 |
--------------------------------------------------------------------------------
/tests/directories.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "directories";
3 |
4 | machine.nixcloud.directories = {
5 | "////foo/./../bar/".owner = "alice";
6 |
7 | "./foo/./../bar/subdir" = {
8 | owner = "root";
9 | group = "vip";
10 | groups.bobs.write = true;
11 | };
12 |
13 | "common/ancestor1".owner = "alice";
14 | "common/ancestor1".group = "bobs";
15 |
16 | "common/ancestor2".owner = "bob";
17 | "common/ancestor2".group = "vip";
18 |
19 | "super/n/e/s/t/e/d" = {
20 | permissions.defaultDirectoryMode = "0700";
21 | owner = "alice";
22 | group = "vip";
23 | postCreate = "id -nu > owner.txt";
24 | postCreateAsRoot = "id -nu > root.txt";
25 | };
26 |
27 | "little/house/of/bob" = {
28 | owner = "bob";
29 | group = "vip";
30 | users.alice = {};
31 | postUpdate = "id -nu > owner.txt";
32 | postUpdateAsRoot = "id -nu > root.txt";
33 | };
34 |
35 | "/only/alice" = {
36 | owner = "alice";
37 | group = "bobs";
38 | permissions.group.noAccess = true;
39 | permissions.others.noAccess = true;
40 | permissions.enableACLs = false;
41 | };
42 | };
43 |
44 | machine.nixcloud.webservices.custom.foo = {
45 | enable = true;
46 |
47 | proxyOptions = {
48 | domain ="foo.de";
49 | port = 8888;
50 | };
51 |
52 | directories."/relative/to/statedir" = {
53 | owner = "alice";
54 | group = "bobs";
55 | };
56 |
57 | runtimeDirectories."relative/to/runtimedir" = {
58 | owner = "bob";
59 | group = "vip";
60 | };
61 |
62 | directories.overlap.owner = "root";
63 | runtimeDirectories.overlap.owner = "alice";
64 | };
65 |
66 | machine.users.groups.vip = {};
67 | machine.users.groups.bobs = {};
68 | machine.users.users.alice.isNormalUser = true;
69 | machine.users.users.bob.isNormalUser = true;
70 | machine.users.users.bob.extraGroups = [ "bobs" ];
71 |
72 | testScript = { nodes, ... }: let
73 | inherit (nodes.machine.config.nixcloud.webservices.custom) foo;
74 | in ''
75 | sub ensureStat ($$$$) {
76 | my ($path, $expect, $desc, $flag) = @_;
77 | my $result = $machine->succeed('stat -c %'.$flag.' '.$path);
78 | chomp $result;
79 | die "$desc for path $path is $result but expected $expect"
80 | unless $result eq $expect;
81 | }
82 |
83 | sub ensureOwner ($$) {
84 | ensureStat $_[0], $_[1], 'owner', 'U';
85 | }
86 |
87 | sub ensureGroup ($$) {
88 | ensureStat $_[0], $_[1], 'group', 'G';
89 | }
90 |
91 | sub ensureMode ($$) {
92 | ensureStat $_[0], $_[1], 'mode', '04a';
93 | }
94 |
95 | sub showPerms ($) {
96 | $machine->execute('ls -lad '.$_[0].' >&2; getfacl '.$_[0].' >&2');
97 | }
98 |
99 | sub checkGenericPermissions {
100 | showPerms "/foo/bar";
101 | showPerms "/foo/bar/subdir";
102 | showPerms "/little/house/of/bob";
103 | showPerms "/super/n/e/s/t/e/d";
104 | showPerms "/only/alice";
105 |
106 | ensureOwner "/foo/bar", "alice";
107 | ensureOwner "/foo/bar/subdir", "root";
108 |
109 | ensureGroup "/foo/bar", "root";
110 | ensureGroup "/foo/bar/subdir", "vip";
111 |
112 | ensureOwner "/little/house/of/bob", "bob";
113 | ensureGroup "/little/house/of/bob", "vip";
114 |
115 | ensureOwner "/super/n/e/s/t/e/d", "alice";
116 | ensureGroup "/super/n/e/s/t/e/d", "vip";
117 |
118 | ensureOwner "/common/ancestor1", "alice";
119 | ensureGroup "/common/ancestor1", "bobs";
120 |
121 | ensureOwner "/common/ancestor2", "bob";
122 | ensureGroup "/common/ancestor2", "vip";
123 |
124 | ensureOwner "/only/alice", "alice";
125 | ensureGroup "/only/alice", "bobs";
126 | ensureMode "/only/alice", "0700";
127 |
128 | ensureOwner "${foo.stateDir}/relative/to/statedir", "alice";
129 | ensureGroup "${foo.stateDir}/relative/to/statedir", "bobs";
130 |
131 | ensureOwner "${foo.runtimeDir}/relative/to/runtimedir", "bob";
132 | ensureGroup "${foo.runtimeDir}/relative/to/runtimedir", "vip";
133 |
134 | ensureOwner "${foo.stateDir}/overlap", "root";
135 | ensureOwner "${foo.runtimeDir}/overlap", "alice";
136 | }
137 |
138 | $machine->waitForUnit('multi-user.target');
139 |
140 | checkGenericPermissions;
141 |
142 | $machine->nest('check if alice can write to a file created by root', sub {
143 | $machine->succeed('echo root > /foo/bar/writable_by_alice');
144 |
145 | showPerms "/foo/bar/writable_by_alice";
146 |
147 | $machine->succeed(
148 | 'su -c "echo alice >> /foo/bar/writable_by_alice" alice',
149 | 'test "$(tr -d \'\\n\' < /foo/bar/writable_by_alice)" = rootalice'
150 | );
151 | });
152 |
153 | $machine->nest("check if alice can write to bob's path", sub {
154 | $machine->succeed(
155 | 'su -c "echo hello > /little/house/of/bob/alice.txt" alice'
156 | );
157 | });
158 |
159 | $machine->nest('check if bob can write to /foo/bar/subdir', sub {
160 | $machine->succeed('su -c "echo test > /foo/bar/subdir/bob.txt" bob');
161 | });
162 |
163 | $machine->nest('check default directory mode', sub {
164 | ensureMode "/super", "0700";
165 | ensureMode "/super/n", "0700";
166 | ensureMode "/super/n/e", "0700";
167 | ensureMode "/super/n/e/s", "0700";
168 | ensureMode "/super/n/e/s/t", "0700";
169 | ensureMode "/super/n/e/s/t/e", "0700";
170 | });
171 |
172 | $machine->nest('check whether postCreate has worked', sub {
173 | ensureOwner "/super/n/e/s/t/e/d/owner.txt", "alice";
174 | ensureGroup "/super/n/e/s/t/e/d/owner.txt", "vip";
175 | ensureOwner "/super/n/e/s/t/e/d/root.txt", "root";
176 | ensureGroup "/super/n/e/s/t/e/d/root.txt", "root";
177 | $machine->succeed('test "$(< /super/n/e/s/t/e/d/owner.txt)" = alice');
178 | $machine->succeed('test "$(< /super/n/e/s/t/e/d/root.txt)" = root');
179 | });
180 |
181 | $machine->nest('check whether postUpdate has worked', sub {
182 | ensureOwner "/little/house/of/bob/owner.txt", "bob";
183 | ensureGroup "/little/house/of/bob/owner.txt", "vip";
184 | ensureOwner "/little/house/of/bob/root.txt", "root";
185 | ensureGroup "/little/house/of/bob/root.txt", "root";
186 | $machine->succeed('test "$(< /little/house/of/bob/owner.txt)" = bob');
187 | $machine->succeed('test "$(< /little/house/of/bob/root.txt)" = root');
188 | });
189 |
190 | $machine->nest('remove postCreate files to check after reboot', sub {
191 | $machine->succeed('rm /super/n/e/s/t/e/d/owner.txt');
192 | $machine->succeed('rm /super/n/e/s/t/e/d/root.txt');
193 | });
194 |
195 | $machine->nest('remove postUpdate files to check after reboot', sub {
196 | $machine->succeed('rm /little/house/of/bob/owner.txt');
197 | $machine->succeed('rm /little/house/of/bob/root.txt');
198 | });
199 |
200 | $machine->nest('rebooting machine', sub {
201 | $machine->shutdown;
202 | $machine->waitForUnit('multi-user.target');
203 | });
204 |
205 | checkGenericPermissions;
206 |
207 | $machine->nest('check whether the file owner has been fixed up', sub {
208 | showPerms "/foo/bar/writable_by_alice";
209 | ensureOwner "/foo/bar/writable_by_alice", "alice";
210 | ensureGroup "/foo/bar/writable_by_alice", "root";
211 | });
212 |
213 | $machine->nest('check whether postCreate ran on existing directory', sub {
214 | $machine->fail('test -e /super/n/e/s/t/e/d/owner.txt');
215 | $machine->fail('test -e /super/n/e/s/t/e/d/root.txt');
216 | });
217 |
218 | $machine->nest('check whether postUpdate ran on existing directory', sub {
219 | $machine->succeed('test -e /little/house/of/bob/owner.txt');
220 | $machine->succeed('test -e /little/house/of/bob/root.txt');
221 | });
222 |
223 | $machine->nest('check whether noAccess modes are applied correctly', sub {
224 | ensureOwner "/only/alice", "alice";
225 | ensureGroup "/only/alice", "bobs";
226 | ensureMode "/only/alice", "0700";
227 | });
228 | '';
229 | }
230 |
--------------------------------------------------------------------------------
/tests/ip2unix.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "ip-to-unix";
3 |
4 | machine = { pkgs, lib, nclib, ... }: let
5 | mkTestWebserver = value: pkgs.writeScript "test-webserver" ''
6 | #!${pkgs.python3.interpreter}
7 | from http.server import BaseHTTPRequestHandler, HTTPServer
8 |
9 | class TestHandler(BaseHTTPRequestHandler):
10 | def do_GET(self):
11 | self.send_response(200)
12 | self.send_header('Content-type', 'text/plain')
13 | self.send_header('Content-length', 7)
14 | self.end_headers()
15 | self.wfile.write('${value}\n'.encode('utf-8'))
16 |
17 | server = HTTPServer(("", 80), TestHandler)
18 | print(server.server_name)
19 | print(server.server_port)
20 | server.serve_forever()
21 | '';
22 | in {
23 | systemd.sockets.test-webserver-socket-activation = {
24 | description = "Socket For Test Webserver";
25 | requiredBy = [ "sockets.target" ];
26 | socketConfig.ListenStream = "/run/test-webserver-activation.socket";
27 | socketConfig.FileDescriptorName = "http";
28 | };
29 |
30 | systemd.services.test-webserver-socket-activation = {
31 | description = "Test Webserver With Socket Activation";
32 | serviceConfig.ExecStart = nclib.ip2unix {
33 | program = mkTestWebserver "barfoo";
34 | rules = lib.singleton {
35 | direction = "incoming";
36 | socketActivation = true;
37 | fdName = "http";
38 | port = 80;
39 | };
40 | };
41 | };
42 |
43 | systemd.services.test-webserver-direct = {
44 | description = "Test Webserver Without Socket Activation";
45 | after = [ "network.target" ];
46 | wantedBy = [ "multi-user.target" ];
47 | serviceConfig.ExecStart = nclib.ip2unix {
48 | program = mkTestWebserver "foobar";
49 | rules = [
50 | { socketPath = "/run/test-webserver-direct.socket";
51 | fdName = "http";
52 | port = 80;
53 | }
54 | ];
55 | };
56 | };
57 |
58 | environment.systemPackages = lib.singleton (nclib.ip2unix {
59 | program = "${pkgs.curl}/bin/curl";
60 | useBinDir = true;
61 | baseName = "sockurl";
62 | rules = [
63 | { direction = "outgoing";
64 | address = "1.2.3.4";
65 | socketPath = "/run/test-webserver-direct.socket";
66 | }
67 | { direction = "outgoing";
68 | address = "4.3.2.1";
69 | socketPath = "/run/test-webserver-activation.socket";
70 | }
71 | ];
72 | });
73 | };
74 |
75 | testScript = ''
76 | $machine->waitForUnit('test-webserver-direct.service');
77 | $machine->waitForFile('/run/test-webserver-direct.socket');
78 | $machine->succeed('test "$(sockurl -vvv http://1.2.3.4/)" = foobar');
79 | $machine->succeed('test "$(sockurl -vvv http://4.3.2.1/)" = barfoo');
80 | '';
81 | }
82 |
--------------------------------------------------------------------------------
/tests/mkunique.nix:
--------------------------------------------------------------------------------
1 | { pkgs, ... }:
2 |
3 | let
4 | inherit (pkgs) lib;
5 |
6 | wsModArgs = (lib.evalModules {
7 | modules = [
8 | { _module.args.wsName = "foo";
9 | _module.args.name = "bar";
10 | }
11 | ../modules/web/core/base.nix
12 | ];
13 | }).config._module.args;
14 |
15 | inherit (wsModArgs) mkUnique mkUniqueUser mkUniqueGroup;
16 |
17 | mkUniqueUserGroupTests = basename: fun: hashPrefix: hashedAtLen: let
18 | generateTestsFor = nestFun: append: isHashed: from: to: let
19 | mkTest = curLen: let
20 | genStr = len: lib.concatStrings (lib.genList (lib.const "x") len);
21 | testStr = genStr curLen;
22 | in {
23 | name = "${basename}${append}${toString curLen}";
24 | value = if isHashed then {
25 | expr = let
26 | unnested = fun testStr;
27 | nested = nestFun unnested;
28 | matchHashed = builtins.match "([^-]+)-[a-f0-9]+";
29 | in {
30 | result = matchHashed nested;
31 | isEqual = nested == unnested;
32 | };
33 | expected.result = [ hashPrefix ];
34 | expected.isEqual = true;
35 | } else {
36 | expr = nestFun (fun testStr);
37 | expected = "foo-bar-${genStr curLen}";
38 | };
39 | };
40 | in lib.listToAttrs (map mkTest (lib.range from to));
41 |
42 | generateTests = nestFun: append: let
43 | genTests = generateTestsFor nestFun;
44 | in genTests "Unhashed${append}" false 0 (hashedAtLen - 1)
45 | // genTests "Hashed${append}" true hashedAtLen 200;
46 |
47 | basic = generateTests lib.id "";
48 | nestedUniqueUser = generateTests mkUniqueUser "NestedMkUniqueUser";
49 | nestedUniqueGroup = generateTests mkUniqueGroup "NestedMkUniqueGroup";
50 | nestedUnique = generateTests mkUnique "NestedMkUnique";
51 |
52 | in basic // nestedUniqueUser // nestedUniqueGroup // nestedUnique;
53 |
54 | in {
55 | name = "mkunique";
56 | type = "unit";
57 |
58 | tests = {
59 | mkUniqueWithWSname = {
60 | expr = mkUnique "foo";
61 | expected = "foo-bar";
62 | };
63 |
64 | mkUniqueWithUniqueName = {
65 | expr = mkUnique "foo-bar";
66 | expected = "foo-bar";
67 | };
68 |
69 | nestedMkUnique = {
70 | expr = mkUnique (mkUnique (mkUnique "xxx"));
71 | expected = "foo-bar-xxx";
72 | };
73 | } // mkUniqueUserGroupTests "mkUniqueUser" mkUniqueUser "user" 24
74 | // mkUniqueUserGroupTests "mkUniqueGroup" mkUniqueGroup "group" 24;
75 | }
76 |
--------------------------------------------------------------------------------
/tests/user-allocation-uid-gid-test.nix:
--------------------------------------------------------------------------------
1 | # test for UID/GID reuse
2 | { pkgs, ... }:
3 | {
4 | name = "user-allocation-uid-gid-test";
5 |
6 | nodes = {
7 |
8 | machine1 = { pkgs, lib, ... }: {
9 | nixcloud.webservices.mediawiki.one = {
10 | enable = true;
11 | proxyOptions = {
12 | port = 5000;
13 | path = "/foo";
14 | domain = "example.com";
15 | };
16 | };
17 | };
18 |
19 | machine2 = { pkgs, lib, ... }: {
20 | nixcloud.webservices.mediawiki.one = {
21 | enable = false;
22 | proxyOptions = {
23 | port = 5000;
24 | path = "/foo";
25 | domain = "example.com";
26 | };
27 | };
28 |
29 | nixcloud.webservices.mediawiki.two = {
30 | enable = true;
31 | proxyOptions = {
32 | port = 5001;
33 | path = "/bar";
34 | domain = "example.com";
35 | };
36 | };
37 | };
38 | };
39 |
40 | testScript = {nodes, ...}: let
41 | m1 = nodes.machine1.config.system.build.toplevel;
42 | m2 = nodes.machine2.config.system.build.toplevel;
43 | in ''
44 | $machine1->waitForUnit('multi-user.target');
45 | $machine1->waitForOpenPort(5000);
46 |
47 | $machine1->succeed('curl localhost:5000 >&2');
48 | $machine1->succeed('ls -lanthr /var/lib/nixcloud/webservices/mediawiki-one >&2');
49 |
50 | $machine1->succeed("${m2}/bin/switch-to-configuration test >&2");
51 | $machine1->waitForOpenPort(5001);
52 | $machine1->succeed('curl localhost:5001 >&2');
53 | $machine1->succeed('cat /etc/passwd >&2');
54 | $machine1->succeed('ls -lanthr /var/lib/nixcloud/webservices/mediawiki-one >&2');
55 | $machine1->succeed('ls -lanthr /var/lib/nixcloud/webservices/mediawiki-two >&2');
56 |
57 | $machine1->succeed('
58 | r1=$(stat -c \'%u\' /var/lib/nixcloud/webservices/mediawiki-one/runtime)
59 | r2=$(stat -c \'%u\' /var/lib/nixcloud/webservices/mediawiki-two/runtime)
60 | echo $r1 >&2
61 | echo $r2 >&2
62 |
63 | if [ "$r1" == "$r2" ]; then
64 | echo "Critical: UID was reused, this indicates a critical error" >&2;
65 | exit 1;
66 | else
67 | echo "UID was not reused which is good" >&2;
68 | exit 0;
69 | fi;
70 | ');
71 | '';
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/tests/version.nix:
--------------------------------------------------------------------------------
1 | {
2 | name = "version";
3 |
4 | machine = {};
5 |
6 | testScript = ''
7 | $machine->waitForUnit('multi-user.target');
8 | $machine->succeed(
9 | 'nixcloud-version | grep -q \'^\(master\|[0-9a-f]\{40\}\)$\''',
10 | 'nixos-version | grep -q nixcloud'
11 | );
12 | '';
13 | }
14 |
--------------------------------------------------------------------------------