├── 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 | ![Unmaintained](https://img.shields.io/badge/status-unmaintained-red) 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: </run/rspamd/worker-controller.sock> 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: </run/rspamd/worker-controller.sock> 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 | <screen> 18 | # doveadm pw -s sha256-crypt 19 | Enter new password: 20 | Retype new password: 21 | {SHA256-CRYPT}$5$/CHK3ckfRJloONnq$X/16jK2NPTiZpBZZ1XVpHhPXyxPy1p0QtUNeUFrYav5 22 | </screen> 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 <link 12 | xlink:href="${url}"><filename>lib/licenses.nix</filename> file</link> 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 <option>environment.systemPackages</option> 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 <option>webserver.user</option>. 38 | 39 | <note><para>These commands are run directly after 40 | <option>serverInit</option> but before the actual 41 | web server.</para></note> 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 | <note><para>These commands are run as the <systemitem 52 | class="username">root</systemitem> user.</para></note> 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 <option>users.users</option> 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 <option>users.groups</option> 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 | <filename>/tmp</filename> instance. 92 | 93 | <warning><para> 94 | If postgresql stores the socket context in <filename>/tmp</filename> 95 | you have to say <literal>false</literal> here or it can't be used at 96 | all. 97 | </para></warning> 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 <<SETUP 12 | from distutils.core import setup 13 | setup(name='test-rabbitmq-${name}', scripts=['test-rabbitmq-${name}']) 14 | SETUP 15 | ''; 16 | src = pkgs.writeTextDir "test-rabbitmq-${name}" '' 17 | #!/usr/bin/env python 18 | ${src} 19 | ''; 20 | }; 21 | in { 22 | config.messaging.rabbitmq.enable = true; 23 | 24 | config.systemd.services.test-send = { 25 | description = "Test Sending to RabbitMQ"; 26 | serviceConfig.Type = "oneshot"; 27 | serviceConfig.ExecStart = "${useRabbitMQ} ${mkPyTest "send" '' 28 | import pika 29 | 30 | cparams = pika.ConnectionParameters(host='localhost') 31 | connection = pika.BlockingConnection(cparams) 32 | channel = connection.channel() 33 | 34 | channel.queue_declare(queue='hello') 35 | 36 | channel.basic_publish(exchange="", routing_key='hello', body='world') 37 | connection.close() 38 | ''}/bin/test-rabbitmq-send"; 39 | }; 40 | 41 | config.systemd.services.test-recv = { 42 | description = "Test Receiving from RabbitMQ"; 43 | serviceConfig.Type = "oneshot"; 44 | serviceConfig.RemainAfterExit = true; 45 | serviceConfig.ExecStart = "${useRabbitMQ} ${mkPyTest "recv" '' 46 | import sys 47 | import pika 48 | 49 | cparams = pika.ConnectionParameters(host='localhost') 50 | connection = pika.BlockingConnection(cparams) 51 | channel = connection.channel() 52 | 53 | channel.queue_declare(queue='hello') 54 | 55 | def callback(ch, method, properties, body): 56 | open('/tmp/received', 'wb').write(body) 57 | raise SystemExit 58 | 59 | channel.basic_consume(callback, queue='hello', no_ack=True) 60 | 61 | open('/tmp/recv-ready', 'w') 62 | channel.start_consuming() 63 | ''}/bin/test-rabbitmq-recv"; 64 | }; 65 | 66 | meta.license = null; 67 | }; 68 | 69 | in { 70 | name = "rabbitmq"; 71 | 72 | machine = { lib, nclib, pkgs, ... }: { 73 | imports = let 74 | mkWebService = import ../../../../lib/make-webservice.nix; 75 | in lib.singleton (mkWebService "rabbitmq-test" rabbitMQWebService); 76 | 77 | nixcloud.webservices.rabbitmq-test.foo.enable = true; 78 | }; 79 | 80 | testScript = '' 81 | $machine->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 <<EOF 11 | <!DOCTYPE html> 12 | <html> 13 | <head> 14 | <meta charset="UTF-8"> 15 | <title>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 | --------------------------------------------------------------------------------