├── .gitignore ├── COPYING ├── LICENSE.APACHE ├── common-machines.nix ├── common.nix ├── dns-server.nix ├── domains.nix ├── hydra-slave.nix ├── hydra.nix ├── lib ├── credentials.nix ├── default.nix ├── erlang │ ├── default.nix │ ├── escape-string.nix │ └── generate.py ├── module-support.nix └── types.nix ├── machines └── ultron │ └── default.nix ├── modules ├── config │ ├── conditions.nix │ ├── erlang-inet │ │ └── default.nix │ ├── imperative-containers.nix │ ├── ipaddr.nix │ ├── postgresql.nix │ └── vhosts.nix ├── core.nix ├── module-list.nix ├── services │ ├── acme │ │ ├── default.nix │ │ └── validator.hs │ ├── dyndns │ │ ├── default.nix │ │ └── dyndns.hs │ ├── epmd │ │ └── default.nix │ ├── lighttpd.nix │ ├── mongooseim │ │ ├── access.nix │ │ ├── acl.nix │ │ ├── ctl.nix │ │ ├── default.nix │ │ ├── lib.nix │ │ ├── listeners.nix │ │ ├── modules.nix │ │ └── settings.nix │ ├── nix-ssh-build.nix │ ├── nsd-zone-writer │ │ ├── default.nix │ │ └── writer.sh.in │ ├── postfix │ │ ├── default.nix │ │ └── postpatch.nix │ └── webspace │ │ ├── default.nix │ │ └── openssh.patch └── testing │ ├── letsencrypt.nix │ ├── network.nix │ ├── nixops.nix │ ├── resolver.nix │ └── tshark.nix ├── network.nix ├── pkgs ├── acmetool │ ├── absolute-symlinks.patch │ ├── default.nix │ ├── desired-in-store.patch │ └── remove-unneded-responders.patch ├── build-support │ ├── build-erlang │ │ ├── default.nix │ │ ├── rebar-nix.patch │ │ ├── rewrite-appfiles.erl │ │ └── rewrite-rebar-config.erl │ ├── compile-c.nix │ ├── compile-haskell.nix │ └── write-escript.nix ├── default.nix ├── erlang-packages.nix ├── mongooseim │ ├── allow-set-default-mam-archive-mode.patch │ ├── ctl-set-config.patch │ ├── default.nix │ ├── dhparams.patch │ ├── domain-certfile-binary.patch │ ├── journald.patch │ ├── pep-mod-caps-dep.patch │ ├── pgsql-disable-escape-string-warning.patch │ ├── reltool.patch │ ├── s2s-listener-certfile.patch │ ├── systemd.patch │ ├── tests │ │ ├── ctl-path.patch │ │ ├── default.nix │ │ ├── dont-tamper-with-config.patch │ │ └── no-fed-node.patch │ └── tls-fixes.patch ├── nexus │ ├── LICENSE │ ├── Nexus │ │ ├── DNS.hs │ │ ├── DNS │ │ │ ├── Types.hs │ │ │ ├── Types │ │ │ │ ├── Domain.hs │ │ │ │ ├── IpAddr.hs │ │ │ │ └── NatInt32.hs │ │ │ └── ZoneBuilder.hs │ │ ├── Process.hs │ │ └── Socket.hsc │ ├── Setup.hs │ ├── SocketTest.hs │ ├── default.nix │ └── nexus.cabal ├── site │ ├── default.nix │ └── frontend │ │ ├── Headcounter.hx │ │ └── gfx │ │ ├── coming_soon.cat │ │ ├── head.cat │ │ └── head_annoyed.cat ├── spectrum2 │ ├── default.nix │ ├── libcommuni.nix │ ├── libpqxx.nix │ ├── swiften.nix │ └── swiften.patch └── xmppoke │ ├── default.nix │ ├── genreport.nix │ └── server-handler.patch ├── release.nix ├── ssl └── snakeoil.nix ├── tests ├── acme.nix ├── code-reload │ ├── default.nix │ └── testclient.erl ├── default.nix ├── dyndns.nix ├── hclib.nix ├── headcounter │ ├── default.nix │ ├── listeners.nix │ └── per-vhost │ │ ├── escalus │ │ ├── default.nix │ │ └── headcounter_SUITE.erl │ │ └── xmppoke.nix ├── make-test.nix ├── mongooseim │ ├── default.nix │ └── lib.nix ├── nsd-zone-writer.nix ├── postfix.nix └── postgresql.nix └── xmpp.nix /.gitignore: -------------------------------------------------------------------------------- 1 | /credentials.nix 2 | /private/ 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Everything except files that end with ".patch" are copyright (C) 2013 2 | aszlig and licensed under the Apache License, Version 2.0 (the 3 | "License"); you may not use these files except in compliance with the 4 | License. You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | 14 | If the URL to the license is unavailable, please see LICENSE.APACHE in 15 | this directory. 16 | 17 | All of the .patch files are copyright (C) 2013 aszlig as well, but are 18 | licensed under the licenses of the corresponding projects. Please look 19 | at accompanying Nix expressions for more information about the 20 | licenses of the respective projects and thus my patches. 21 | -------------------------------------------------------------------------------- /common-machines.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, ... }: 2 | 3 | let 4 | vuizvui = (import {}).fetchFromGitHub { 5 | owner = "openlab-aux"; 6 | repo = "vuizvui"; 7 | rev = "9e235ef0f0d48f241ec6b0bcb7b332c182c2aadf"; 8 | sha256 = "1ja2nc3q0c4il6f507mlxig5fzl3j384asnx3jb89r7jjryjm0jr"; 9 | }; 10 | 11 | vim = "${vuizvui}/modules/user/aszlig/programs/vim/default.nix"; 12 | 13 | in { 14 | imports = [ ./common.nix vim ]; 15 | 16 | vuizvui.user.aszlig.programs.vim.enable = true; 17 | 18 | deployment.targetEnv = lib.mkOverride 900 "hetzner"; 19 | 20 | environment.systemPackages = with pkgs; [ 21 | atop htop iotop 22 | sysstat dstat 23 | smartmontools 24 | perf-tools 25 | netrw 26 | ]; 27 | 28 | networking.extraHosts = '' 29 | 136.243.109.98 falayalaralfali 30 | ''; 31 | 32 | services.openntpd.enable = true; 33 | 34 | nix = { 35 | package = pkgs.nixUnstable; 36 | nrBuildUsers = 100; 37 | useSandbox = true; 38 | readOnlyStore = true; 39 | buildCores = 0; 40 | }; 41 | 42 | nixpkgs.config.allowUnfree = true; 43 | hardware.cpu.intel.updateMicrocode = true; 44 | 45 | time.timeZone = "Europe/Berlin"; 46 | } 47 | -------------------------------------------------------------------------------- /common.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | 3 | { 4 | imports = import ./modules/module-list.nix; 5 | networking.firewall.enable = false; 6 | 7 | services.journald.extraConfig = '' 8 | MaxRetentionSec=3month 9 | ''; 10 | 11 | headcounter.services.acme = { 12 | key.type = "rsa"; 13 | key.size = 4096; 14 | }; 15 | 16 | nixpkgs.overlays = lib.singleton (self: lib.const { 17 | inherit (import ./pkgs { pkgs = self; }) headcounter; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /dns-server.nix: -------------------------------------------------------------------------------- 1 | { config, lib, nodes, ... }: 2 | 3 | { 4 | deployment.hetzner.partitions = '' 5 | clearpart --all --initlabel --drives=vda 6 | part swap --size=2000 --label=swap --fstype=swap --ondisk=vda 7 | part / --fstype=ext4 --label=root --grow --ondisk=vda 8 | ''; 9 | 10 | headcounter.services.dyndns.slave = { 11 | enable = true; 12 | useNSD = true; 13 | master = let 14 | myself = config.networking.hostName; 15 | tunnel = nodes.ultron.config.networking.p2pTunnels.ssh.${myself}; 16 | in lib.singleton { 17 | host = tunnel.remoteIPv4; 18 | device = "tun${toString tunnel.remoteTunnel}"; 19 | }; 20 | }; 21 | 22 | services.nsd = let 23 | primaryDNS = "ns1.headcounter.org"; 24 | secondaryDNS = "ns2.headcounter.org"; 25 | 26 | mkSOA = dottedEmail: '' 27 | @ IN SOA ${primaryDNS}. ${dottedEmail}. ( 28 | 0 ; Serial 29 | 28800 ; Refresh 30 | 7200 ; Retry 31 | 604800 ; Expire 32 | 86400 ; Negative Cache TTL 33 | ) 34 | ''; 35 | 36 | # FIXME: torservers.net needs to be handled differently here! 37 | mkZone = domain: text: '' 38 | $ORIGIN ${domain}. 39 | $TTL 60 40 | ${mkSOA "postmaster.${domain}"} 41 | @ IN NS ${primaryDNS}. 42 | @ IN NS ${secondaryDNS}. 43 | @ IN MX 10 mailfresser.de. 44 | 45 | _acme-challenge IN NS ${primaryDNS}. 46 | 47 | ${text} 48 | ''; 49 | 50 | mkXMPPRecords = dest: '' 51 | conference IN CNAME ${dest} 52 | irc IN CNAME ${dest} 53 | pubsub IN CNAME ${dest} 54 | vjud IN CNAME ${dest} 55 | icq IN CNAME ${dest} 56 | proxy IN CNAME ${dest} 57 | 58 | _jabber._tcp IN SRV 10 0 5269 ${dest} 59 | _xmpp-server._tcp IN SRV 10 0 5269 ${dest} 60 | _xmpp-client._tcp IN SRV 10 0 5222 ${dest} 61 | ''; 62 | 63 | # XXX: Don't hardcode ultron here! 64 | inherit (nodes.ultron.config.headcounter) vhosts; 65 | 66 | in { 67 | enable = true; 68 | interfaces = lib.mkForce []; # all interfaces 69 | zones = { 70 | "aszlig.net.".data = mkZone "aszlig.net" '' 71 | ; Generic stuff 72 | @ IN A ${vhosts.aszlig.ipv4} 73 | @ IN AAAA ${vhosts.aszlig.ipv6} 74 | 75 | ; Legacy DNS servers 76 | ns1 IN A 85.10.206.212 77 | ns2 IN A 217.20.112.88 78 | 79 | ${mkXMPPRecords "@"} 80 | ''; 81 | 82 | "headcounter.org.".data = mkZone "headcounter.org" '' 83 | ; Generic stuff 84 | @ IN A ${vhosts.headcounter.ipv4} 85 | @ IN AAAA ${vhosts.headcounter.ipv6} 86 | 87 | ; Hosts of the new deployment 88 | taalo IN A 188.40.96.202 89 | taalo IN AAAA 2a01:4f8:221:17c6:: 90 | benteflork IN A 144.76.202.147 91 | benteflork IN AAAA 2a01:4f8:200:8392:: 92 | ultron IN A 5.9.105.142 93 | ultron IN AAAA 2a01:4f8:162:4187:: 94 | 95 | ; DNS servers 96 | ns1 IN A 78.46.182.124 97 | ns1 IN AAAA 2a01:4f8:d13:3009::2 98 | ns2 IN A 78.47.142.38 99 | ns2 IN AAAA 2a01:4f8:d13:5308::2 100 | 101 | ${lib.concatMapStringsSep "\n" (name: '' 102 | ${name} IN NS ${primaryDNS}. 103 | ${name} IN NS ${secondaryDNS}. 104 | '') [ "zrnzrk" "docsnyder" ]} 105 | 106 | ${mkXMPPRecords "@"} 107 | ''; 108 | 109 | "no-icq.org.".data = mkZone "no-icq.org" '' 110 | ; Generic stuff 111 | @ IN A ${vhosts.no_icq.ipv4} 112 | @ IN AAAA ${vhosts.no_icq.ipv6} 113 | 114 | ${mkXMPPRecords "@"} 115 | ''; 116 | 117 | "noicq.org.".data = mkZone "noicq.org" '' 118 | ; Generic stuff 119 | @ IN A ${vhosts.noicq.ipv4} 120 | @ IN AAAA ${vhosts.noicq.ipv6} 121 | 122 | ${mkXMPPRecords "@"} 123 | ''; 124 | 125 | "jabber.torservers.net.".data = mkZone "jabber.torservers.net" '' 126 | ; Generic stuff 127 | @ IN A ${vhosts.torservers.ipv4} 128 | @ IN AAAA ${vhosts.torservers.ipv6} 129 | 130 | ${mkXMPPRecords "@"} 131 | ''; 132 | }; 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /domains.nix: -------------------------------------------------------------------------------- 1 | { 2 | config.headcounter.vhosts = { 3 | headcounter = { 4 | fqdn = "headcounter.org"; 5 | ipv4 = "78.47.32.129"; 6 | ipv6 = "2a01:4f8:162:4187::1"; 7 | isXMPP = true; 8 | }; 9 | 10 | torservers = { 11 | fqdn = "torservers.net"; 12 | ipv4 = "78.47.32.130"; 13 | ipv6 = "2a01:4f8:162:4187::2"; 14 | isXMPP = true; 15 | }; 16 | 17 | aszlig = { 18 | fqdn = "aszlig.net"; 19 | ipv4 = "78.47.32.131"; 20 | ipv6 = "2a01:4f8:162:4187::3"; 21 | isXMPP = true; 22 | }; 23 | 24 | noicq = { 25 | fqdn = "noicq.org"; 26 | ipv4 = "78.47.32.132"; 27 | ipv6 = "2a01:4f8:162:4187::4"; 28 | isXMPP = true; 29 | }; 30 | 31 | no_icq = { 32 | fqdn = "no-icq.org"; 33 | ipv4 = "78.47.32.133"; 34 | ipv6 = "2a01:4f8:162:4187::5"; 35 | isXMPP = true; 36 | }; 37 | 38 | misc = { 39 | ipv4 = "78.47.32.134"; 40 | ipv6 = "2a01:4f8:162:4187::6"; 41 | }; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /hydra-slave.nix: -------------------------------------------------------------------------------- 1 | { lib, resources, ... }: 2 | 3 | let 4 | nixosSigningKey = '' 5 | -----BEGIN PUBLIC KEY----- 6 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvXmUhovC36OJ9a/3oTaV 7 | 2IvS5gvD87fyG64W/iFKlz2nKWi0JDxq58DNZtaIxSL4sVzQ4pO3M+2Zpgs8+sZ4 8 | x2mekrlk78VAz3qy8ZZRr0ksw51kL/Ub829ajgnG+2sFedmBBhnRTtGtxLikDQ/D 9 | x4g4XMgaa11lmobmmwv5mN1ygG0v2GC04lwuMfNNgOCaOTaszRRRKFEdrPhKfHRW 10 | Ns2nflWz990H/3ohfEDdDMYJ8VBsolI78KrjnttfXu2tqxj7lXZL9XJ1R4vPkpcC 11 | H3JFbERPXgqIFVxbasYWidpfil/sHjVZgyv2l9CznVDet2eDDgI5NgJQjR+S/wHz 12 | owIDAQAB 13 | -----END PUBLIC KEY----- 14 | ''; 15 | 16 | in { 17 | deployment.hetzner.partitions = '' 18 | clearpart --all --initlabel --drives=sda,sdb 19 | 20 | part raid.11 --size=500 --ondisk=sda 21 | part raid.12 --size=500 --ondisk=sdb 22 | 23 | part swap1 --size=20000 --label=swap1 --fstype=swap --ondisk=sda 24 | part swap2 --size=20000 --label=swap2 --fstype=swap --ondisk=sdb 25 | 26 | part raid.21 --grow --ondisk=sda 27 | part raid.22 --grow --ondisk=sdb 28 | 29 | raid /boot --level=1 --device=md0 --fstype=ext2 --label=boot raid.11 raid.12 30 | raid / --level=0 --device=md1 --fstype=ext4 --label=root raid.21 raid.22 31 | ''; 32 | 33 | users.extraUsers.hydrabuild = { 34 | uid = 1000; 35 | description = "Hydra build user"; 36 | useDefaultShell = true; 37 | openssh.authorizedKeys.keys = let 38 | mkServe = key: "command=\"nix-store --serve --write\" ${key}"; 39 | in lib.singleton (mkServe resources.sshKeyPairs.hydra-build.publicKey); 40 | }; 41 | 42 | nix.trustedUsers = [ "hydrabuild" ]; 43 | 44 | environment.etc."nix/signing-key.pub".text = nixosSigningKey; 45 | 46 | systemd.services."enable-ksm" = { 47 | description = "Enable Kernel Same-Page Merging"; 48 | wantedBy = [ "multi-user.target" ]; 49 | after = [ "systemd-udev-settle.service" ]; 50 | script = '' 51 | if [ -e /sys/kernel/mm/ksm ]; then 52 | echo 1 > /sys/kernel/mm/ksm/run 53 | fi 54 | ''; 55 | }; 56 | 57 | nix.gc.automatic = true; 58 | nix.gc.dates = "0/6:0"; 59 | nix.gc.options = let 60 | minSpace = 200; # the minimum of free space we want to have available in GB 61 | avail = "$(df -k -BG --output=avail /nix/store | sed -ne '$s/[^0-9]//gp')"; 62 | maxFreed = "${toString minSpace} - ${avail}"; 63 | in "--max-freed \"$(((${maxFreed}) * 1024 ** 3))\""; 64 | } 65 | -------------------------------------------------------------------------------- /hydra.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, nodes, resources, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | pkgsReimport = import {}; 7 | 8 | hydraSrc = pkgsReimport.stdenv.mkDerivation rec { 9 | name = "hydra-source-${toString revCount}"; 10 | 11 | rev = "2cdc84f34f4de647dd89c5ef503782a3a48ff623"; 12 | revCount = 2462; 13 | 14 | src = pkgsReimport.fetchFromGitHub { 15 | repo = "hydra"; 16 | owner = "NixOS"; 17 | inherit rev; 18 | sha256 = "1gcp22ldyc914aik4yhlzy60ym7z8513pvp0ag5637j44nz0rf7h"; 19 | }; 20 | 21 | phases = [ "unpackPhase" "patchPhase" "installPhase" ]; 22 | 23 | postPatch = let 24 | betterBoehm = "(boehmgc.override { enableLargeConfig = true; })"; 25 | in '' 26 | # Make sure Hydra uses boehm with enableLargeConfig 27 | sed -i -e '/nix = .*;/ { 28 | c nix = nixUnstable.override { boehmgc = ${betterBoehm}; }; 29 | }' -e 's/boehmgc/${betterBoehm}/g' \ 30 | -e 's/w3m\|guile/(&.override { boehmgc = ${betterBoehm}; })/g' \ 31 | release.nix 32 | 33 | # Workaround for poor-mans XSRF protection only taking into account 34 | # Hydras running on / but not subpaths. 35 | sed -i -e '/#.*XSRF protection/ { 36 | :l; n; /}/b 37 | s!^\( *my *\$base * =\).*!\1 '\'''https://headcounter.org/'\''';! 38 | /die\>.*\$base/d 39 | bl 40 | }' src/lib/Hydra/Controller/Root.pm 41 | 42 | # https://github.com/NixOS/hydra/pull/517 43 | sed -i -e "s/\\\''${LOGNAME/'''&/p" release.nix 44 | ''; 45 | 46 | installPhase = "cp -r . $out"; 47 | }; 48 | 49 | hydraRelease = import "${hydraSrc}/release.nix" { 50 | inherit hydraSrc; 51 | officialRelease = true; 52 | }; 53 | 54 | hydraModule = import "${hydraSrc}/hydra-module.nix"; 55 | 56 | hydra = builtins.getAttr config.nixpkgs.system hydraRelease.build; 57 | 58 | buildUser = "hydrabuild"; 59 | 60 | isBuildNode = name: node: hasAttr buildUser node.config.users.extraUsers; 61 | buildNodes = filterAttrs isBuildNode nodes; 62 | 63 | buildKey = resources.sshKeyPairs."hydra-build".privateKey; 64 | in { 65 | imports = singleton hydraModule; 66 | 67 | services.hydra-dev = { 68 | package = hydra; 69 | enable = true; 70 | hydraURL = "https://headcounter.org/hydra/"; 71 | notificationSender = "hydra@headcounter.org"; 72 | listenHost = "localhost"; 73 | extraConfig = '' 74 | binary_cache_secret_key_file = /run/keys/binary-cache.secret 75 | ''; 76 | }; 77 | 78 | users.extraUsers.hydra-www.extraGroups = [ "keys" ]; 79 | users.extraUsers.hydra-queue-runner.extraGroups = [ "keys" ]; 80 | systemd.services.hydra-init.requires = [ "keys.target" ]; 81 | systemd.services.hydra-init.after = [ "keys.target" ]; 82 | 83 | nix.distributedBuilds = true; 84 | nix.buildMachines = flip mapAttrsToList buildNodes (hostName: node: { 85 | inherit hostName; 86 | inherit (node.config.nix) maxJobs; 87 | systems = if node.config.nixpkgs.system == "x86_64-linux" 88 | then [ "i686-linux" "x86_64-linux" ] 89 | else singleton node.config.nixpkgs.system; 90 | sshKey = "/run/keys/buildkey.priv"; 91 | sshUser = buildUser; 92 | supportedFeatures = [ "kvm" "nixos-test" "big-parallel" ]; 93 | }) ++ [ 94 | { hostName = "falayalaralfali"; 95 | maxJobs = 8; 96 | systems = [ "armv6l-linux" "armv7l-linux" ]; 97 | sshKey = "/run/keys/buildkey.priv"; 98 | sshUser = buildUser; 99 | supportedFeatures = [ "kvm" "nixos-test" "big-parallel" ]; 100 | } 101 | ]; 102 | 103 | nix.extraOptions = '' 104 | gc-keep-outputs = true 105 | gc-keep-derivations = true 106 | ''; 107 | 108 | deployment.keys."binary-cache.secret" = { 109 | text = (import ./ssl/hydra.nix).secret; 110 | user = "hydra-www"; 111 | permissions = "0400"; 112 | }; 113 | 114 | deployment.keys."buildkey.priv" = { 115 | text = buildKey; 116 | user = "hydra-queue-runner"; 117 | permissions = "0400"; 118 | }; 119 | 120 | deployment.keys."signkey.priv".text = readFile ./ssl/signing-key.sec; 121 | deployment.storeKeysOnMachine = false; 122 | 123 | environment.etc."nix/signing-key.sec".source = "/run/keys/signkey.priv"; 124 | } 125 | -------------------------------------------------------------------------------- /lib/credentials.nix: -------------------------------------------------------------------------------- 1 | lib: 2 | 3 | rec { 4 | hasCredentials = builtins.pathExists ../credentials.nix; 5 | 6 | getcred = sym: default: let 7 | path = if builtins.isString sym then lib.singleton sym else sym; 8 | pathStr = lib.concatStringsSep "." path; 9 | suicide = throw "Can't find attribute path `${pathStr}' in credentials."; 10 | creds = import ../credentials.nix; 11 | in if hasCredentials then lib.attrByPath path suicide creds else default; 12 | } 13 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | { lib ? import }: 2 | 3 | lib.fold (path: acc: lib.recursiveUpdate acc (import path lib)) {} [ 4 | ./erlang ./module-support.nix ./credentials.nix ./types.nix 5 | ] 6 | -------------------------------------------------------------------------------- /lib/erlang/default.nix: -------------------------------------------------------------------------------- 1 | lib: 2 | 3 | let 4 | inherit (lib) stringToCharacters lowerChars upperChars numbers escape toInt; 5 | inherit (lib) head tail any all range mapAttrsToList concatStringsSep filter; 6 | inherit (lib) isList isAttrs replaceStrings toLower last optional singleton; 7 | inherit (lib) init take drop genList const length foldl' zipLists findFirst; 8 | inherit (lib) mkOptionType mergeOneOption; 9 | 10 | in rec { 11 | erlAtom = val: let 12 | numbers = stringToCharacters "0123456789"; 13 | allowed = lowerChars ++ upperChars ++ numbers ++ ["@" "_"]; 14 | inRange = range: chr: any (c: c == chr) range; 15 | needsEscape = chrList: !inRange lowerChars (head chrList) || 16 | !all (inRange allowed) chrList; 17 | in if needsEscape (stringToCharacters val) 18 | then "'${escape ["'"] val}'" 19 | else val; 20 | 21 | erlString = val: "\"${import ./escape-string.nix (toString val)}\""; 22 | erlBinary = val: "<<${erlString val}>>"; 23 | erlInt = toString; 24 | erlBool = val: erlAtom (if val then "true" else "false"); 25 | erlList = val: "[${concatStringsSep ", " (map toErl val)}]"; 26 | erlTuple = val: "{${concatStringsSep ", " (map toErl val)}}"; 27 | 28 | erlPropListTerms = val: let 29 | mkExTuple = name: extension: erlTuple ([{ atom = name; }] ++ extension); 30 | mkFreeKey = key: value: "{${toErl key}, ${toErl value}}"; 31 | mkMulti = name: value: 32 | if isAttrs value && value ? ${name} then mkTuple name value.${name} 33 | else if isAttrs value then mkTuple name value 34 | else singleton "{${erlAtom name}, ${toErl value}}"; 35 | mkTuple = name: value: 36 | if value ? flag then optional value.flag (erlAtom name) 37 | else if value ? multi then lib.concatMap (mkMulti name) value.multi 38 | else if value ? extuple then singleton (mkExTuple name value.extuple) 39 | else if value ? freekey && value ? value 40 | then singleton (mkFreeKey value.freekey value.value) 41 | else singleton "{${erlAtom name}, ${toErl value}}"; 42 | in lib.concatLists (mapAttrsToList mkTuple val); 43 | 44 | erlPropList = val: "[${concatStringsSep ", " (erlPropListTerms val)}]"; 45 | erlTermList = val: concatStringsSep ".\n" (erlPropListTerms val) + ".\n"; 46 | 47 | toErl = val: let 48 | nonNix = attr: 49 | if attr ? __raw then attr.__raw 50 | else if attr ? atom then erlAtom attr.atom 51 | else if attr ? tuple then erlTuple attr.tuple 52 | else if attr ? binary then erlBinary attr.binary 53 | else erlPropList attr; 54 | nix = term: 55 | if builtins.isInt term then toString term 56 | else if builtins.isBool term then erlBool term 57 | else if builtins.isString term then erlString term 58 | else if isList term then erlList term 59 | else throw ("Can't transform value (${lib.showVal term}) into an " + 60 | "Erlang expression!"); 61 | in (if isAttrs val then nonNix else nix) val; 62 | 63 | types.erlPropList = mkOptionType { 64 | name = "an Erlang property list"; 65 | check = lib.types.attrs.check; 66 | merge = loc: defs: erlPropList (lib.types.attrs.merge loc defs); 67 | }; 68 | 69 | erlType = valType: mkOptionType { 70 | name = "an Erlang expression"; 71 | merge = loc: defs: valType (mergeOneOption loc defs); 72 | }; 73 | 74 | parseErlIpAddr = addr: let 75 | throwInvalid = throw "Invalid IPv4 or IPv6 address: ${addr}"; 76 | tuplize = val: "{${concatStringsSep ", " val}}"; 77 | 78 | v4DigitRe = "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])"; 79 | v6DigitRe = "([0-9a-fA-F]{0,4})"; 80 | v4Re = "${v4DigitRe}\\.${v4DigitRe}\\.${v4DigitRe}\\.${v4DigitRe}"; 81 | v4Parse = builtins.match v4Re addr; 82 | 83 | v6Expanded = let 84 | splitted = let 85 | sanityCheck = builtins.match ".*(PAD|:::).*" addr != null; 86 | splitter = val: let 87 | result = builtins.match "([^:]*):(.*)" val; 88 | iter = [ (head result) ] ++ splitter (last result); 89 | in if result == null then [ val ] else iter; 90 | prepared = splitter (replaceStrings ["::"] [":PAD:"] addr); 91 | tooManyPads = length (filter (x: x == "PAD") prepared) > 1; 92 | simple = if tooManyPads || sanityCheck then throwInvalid else prepared; 93 | v4mapped = builtins.match v4Re (last simple); 94 | rewritten = init simple ++ [ (take 2 v4mapped) (drop 2 v4mapped) ]; 95 | in if v4mapped != null then rewritten else simple; 96 | folder = acc: digit: let 97 | afterAcc = length splitted - length acc; 98 | accLen = length acc; 99 | invalid = padLen < 1 && afterAcc > 2 && accLen > 1; 100 | padLen = 8 - accLen - afterAcc + 1; 101 | pad = genList (const "0") (if invalid then throwInvalid else padLen); 102 | in if digit == "PAD" then acc ++ pad else acc ++ [ digit ]; 103 | result = foldl' folder [] splitted; 104 | in if length result != 8 then throwInvalid else result; 105 | 106 | v6Parse = let 107 | digitizeV6 = digit: let 108 | chars = stringToCharacters (toLower digit); 109 | padded = genList (const "0") (4 - length chars) ++ chars; 110 | table = zipLists (stringToCharacters "0123456789abcdef") (range 0 15); 111 | toDec = c: (findFirst (conv: conv.fst == c) throwInvalid table).snd; 112 | d1 = toDec (head padded); 113 | d2 = toDec (head (tail padded)); 114 | d3 = toDec (last (init padded)); 115 | d4 = toDec (last padded); 116 | converted = toString (d1 * 4096 + d2 * 256 + d3 * 16 + d4); 117 | in if digit == "" then "0" else converted; 118 | 119 | digitizeV4 = d: toString (toInt (head d) * 256 + toInt (last d)); 120 | 121 | convert = digit: let 122 | result = builtins.match v6DigitRe digit; 123 | v6 = if result != null then digitizeV6 (head result) else throwInvalid; 124 | in if isList digit then digitizeV4 digit else v6; 125 | in tuplize (map convert v6Expanded); 126 | 127 | in if v4Parse != null then tuplize v4Parse else v6Parse; 128 | 129 | shErlEsc = escaper: str: let 130 | doubleSlashed = escape ["\\"] (escaper str); 131 | shQuoted = replaceStrings ["'"] [("'\\'" + "'")] doubleSlashed; 132 | in "'${shQuoted}'"; 133 | } 134 | -------------------------------------------------------------------------------- /lib/erlang/escape-string.nix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/headcounter/deployment/c74d9b63a4aa7c0c91b6f0aca1c72b6db3e91b1b/lib/erlang/escape-string.nix -------------------------------------------------------------------------------- /lib/erlang/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | def mkreplace(char): 5 | b = bytes([char]) 6 | if b == b"\"": 7 | inb = b"\\\"" 8 | outb = b"\\\\\\\"" 9 | elif b == b"\n": 10 | inb = b"\\n" 11 | outb = b"\\\\n" 12 | elif b == b"\r": 13 | inb = b"\\r" 14 | outb = b"\\\\r" 15 | elif b == b"\t": 16 | inb = b"\\t" 17 | outb = b"\\\\t" 18 | elif b == b"\\": 19 | inb = b"\\\\" 20 | outb = b"\\\\\\\\" 21 | else: 22 | inb = b 23 | outb = b"\\\\x" + b.hex().encode('ascii') 24 | return (b"\"" + inb + b"\"", b"\"" + outb + b"\"") 25 | 26 | 27 | def write_replacer(what, where): 28 | where.write(b"\t") 29 | for n, item in enumerate(what): 30 | if n % 9 == 0: 31 | if n > 0: 32 | where.write(b"\n\t") 33 | where.write(item) 34 | if (n + 1) % 9 != 0 and n != len(what) - 1: 35 | where.write(b"\t") 36 | where.write(b"\n") 37 | 38 | 39 | with open('escape-string.nix', 'wb') as cf: 40 | cf.write(b"# vi:ts=8:\n") 41 | crange = list(range(1, 32)) + [34, 92] + list(range(127, 256)) 42 | rep_from, rep_to = zip(*[mkreplace(i) for i in crange]) 43 | cf.write(b"builtins.replaceStrings [\n") 44 | write_replacer(rep_from, cf) 45 | cf.write(b"] [\n") 46 | write_replacer(rep_to, cf) 47 | cf.write(b"]\n") 48 | -------------------------------------------------------------------------------- /lib/module-support.nix: -------------------------------------------------------------------------------- 1 | lib: 2 | 3 | { 4 | enumDoc = attrs: '' 5 | 6 | ${lib.concatStrings (lib.flip lib.mapAttrsToList attrs (option: doc: '' 7 | 8 | 9 | ${doc} 10 | 11 | ''))} 12 | 13 | ''; 14 | 15 | # An option for specifying multiple listeners for use in socket activated 16 | # services. 17 | mkListenerOption = desc: defHost: defPort: lib.mkOption { 18 | type = lib.types.listOf (lib.types.submodule { 19 | options.host = lib.mkOption { 20 | type = lib.types.str; 21 | example = "::"; 22 | description = '' 23 | Hostname, IPv4 or IPv6 address to listen for ${desc}. 24 | ''; 25 | }; 26 | 27 | options.port = lib.mkOption { 28 | type = (import ./types.nix lib).types.port; 29 | default = defPort; 30 | description = "Port to listen for ${desc}."; 31 | }; 32 | 33 | options.device = lib.mkOption { 34 | type = lib.types.nullOr lib.types.str; 35 | default = null; 36 | example = "tun1000"; 37 | description = "The network device to bind to for ${desc}."; 38 | }; 39 | }); 40 | 41 | default = lib.singleton { host = defHost; }; 42 | example = [ 43 | { host = "localhost"; } 44 | { host = "1.2.3.4"; port = 666; device = "eth0"; } 45 | ]; 46 | 47 | description = "Hosts/Ports/Devices to listen for ${desc}."; 48 | }; 49 | 50 | # The counterpart to 'mkListenerOption', which creates the actual socket 51 | # units. 52 | # 53 | # For example: 54 | # 55 | # systemd.sockets = hclib.mkSocketConfig { 56 | # namePrefix = "my-shiny-daemon"; 57 | # description = "Socket For My Shiny Daemon"; 58 | # config = config.services.my-shiny-daemon.listen; 59 | # }; 60 | # 61 | mkSocketConfig = 62 | # The name prefix for the socket unit, which will be suffixed with a 63 | # number. For example if the namePrefix is "foo", the first socket unit 64 | # will be called "foo-1.socket". 65 | { namePrefix 66 | # A description for the socket unit. 67 | , description 68 | # An optional file descriptor name used for $LISTEN_FDNAMES. 69 | # See sd_listen_fds_with_names(3) for more information. 70 | , fdName ? null 71 | # The unit name (without .service suffix) of the service requiring the 72 | # sockets. 73 | , service ? namePrefix 74 | # The option definitions of the option declaration made with 75 | # mkListenerOption. 76 | , config 77 | # Extra attributes to add to the socketConfig attribute of the sockets. 78 | , extraSocketConfig ? {} 79 | }: 80 | 81 | builtins.listToAttrs (lib.imap (number: lcfg: { 82 | name = "${namePrefix}-${toString number}"; 83 | value = let 84 | isV6 = builtins.match ".*:.*" lcfg.host != null; 85 | host = if isV6 then "[${lcfg.host}]" else lcfg.host; 86 | hostPort = "${host}:${toString lcfg.port}"; 87 | in { 88 | description = "${description} (${hostPort})"; 89 | wantedBy = let 90 | devUnit = "sys-subsystem-net-devices-${lcfg.device}.device"; 91 | target = if lcfg.device == null then "sockets.target" else devUnit; 92 | in lib.singleton target; 93 | requiredBy = lib.singleton "${service}.service"; 94 | socketConfig = { 95 | # FreeBind is needed, because even while the device is up, an IP 96 | # address might not have been assigned yet. 97 | FreeBind = true; 98 | Service = "${service}.service"; 99 | ListenStream = hostPort; 100 | } // lib.optionalAttrs (lcfg.device != null) { 101 | BindToDevice = lcfg.device; 102 | } // lib.optionalAttrs (fdName != null) { 103 | FileDescriptorName = fdName; 104 | } // extraSocketConfig; 105 | }; 106 | }) config); 107 | } 108 | -------------------------------------------------------------------------------- /lib/types.nix: -------------------------------------------------------------------------------- 1 | lib: 2 | 3 | { 4 | types.port = lib.mkOptionType { 5 | name = "TCP port"; 6 | check = p: lib.isInt p && p <= 65535 && p >= 0; 7 | merge = lib.mergeOneOption; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /modules/config/erlang-inet/default.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, hclib, ... }: 2 | 3 | let 4 | cfg = config.headcounter.erlang-inet; 5 | 6 | # In cfg.hosts we have { host = ip; }, but for the Erlang inetrc we need to 7 | # have it the other way around: { ip = [ host1 host2 ... ]; } 8 | reversedHosts = let 9 | accumHosts = host: acc: [host] ++ lib.remove host acc; 10 | reverse = name: value: { ${value} = name; }; 11 | in lib.foldAttrs accumHosts [] (lib.mapAttrsToList reverse cfg.hosts); 12 | 13 | in { 14 | options.headcounter.erlang-inet = { 15 | hosts = lib.mkOption { 16 | type = with lib.types; attrsOf str; 17 | default = {}; 18 | example.foo = "192.168.0.1"; 19 | example.bar = "192.168.0.2"; 20 | description = '' 21 | A map of host names to IP addresses that is used for looking up short 22 | names in Erlang distributed nodes. 23 | ''; 24 | }; 25 | 26 | inetConfigFile = lib.mkOption { 27 | type = lib.types.path; 28 | internal = true; 29 | description = '' 30 | The generated Erlang inet configuration (ERL_INETRC). 31 | ''; 32 | }; 33 | }; 34 | 35 | config = lib.mkIf (cfg.hosts != {}) { 36 | headcounter.erlang-inet.inetConfigFile = pkgs.writeText "erl_inetrc" '' 37 | {hosts_file, ""}. 38 | ${lib.concatStrings (lib.mapAttrsToList (ip: hosts: '' 39 | {host, ${hclib.parseErlIpAddr ip}, ${hclib.erlList hosts}}. 40 | '') reversedHosts)} 41 | {lookup, [file, native]}. 42 | ''; 43 | }; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /modules/config/ipaddr.nix: -------------------------------------------------------------------------------- 1 | { options, config, lib, ... }: 2 | 3 | { 4 | options.headcounter.mainIPv4 = lib.mkOption { 5 | type = lib.types.nullOr lib.types.str; 6 | default = null; 7 | description = '' 8 | The main IPv4 address of this host. 9 | 10 | This is mainly useful for deployment-wide usage on other nodes, like for 11 | example for DNS records. 12 | 13 | This in turn also sets . 14 | ''; 15 | }; 16 | 17 | options.headcounter.mainIPv6 = lib.mkOption { 18 | type = lib.types.nullOr lib.types.str; 19 | default = null; 20 | description = '' 21 | The main IPv6 address of this host. 22 | 23 | This is mainly useful for deployment-wide usage on other nodes, like for 24 | example for DNS records. 25 | ''; 26 | }; 27 | 28 | options.headcounter.mainDevice = lib.mkOption { 29 | type = lib.types.str; 30 | default = "eth0"; 31 | description = '' 32 | The network device where the and 33 | the address reside. 34 | ''; 35 | }; 36 | 37 | config = let 38 | maybeConfig = lib.optionalAttrs (options ? deployment) { 39 | deployment.hetzner.mainIPv4 = config.headcounter.mainIPv4; 40 | }; 41 | in lib.mkIf (config.headcounter.mainIPv4 != null) maybeConfig; 42 | } 43 | -------------------------------------------------------------------------------- /modules/config/vhosts.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, ssl, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | cfg = config.headcounter; 7 | inherit (config.headcounter) mainDevice; 8 | 9 | # XXX: Parse the domain names and figure out the *real* root. 10 | getRootDomain = dcfg: head dcfg.ssl.domains; 11 | getOtherDomains = dcfg: tail dcfg.ssl.domains; 12 | 13 | domainSubmodule = { config, ... }: { 14 | options = { 15 | fqdn = mkOption { 16 | default = null; 17 | example = "example.com"; 18 | type = types.nullOr (types.uniq types.str); 19 | description = '' 20 | The fully qualified domain name. 21 | ''; 22 | }; 23 | 24 | ipv4 = mkOption { 25 | default = null; 26 | type = types.nullOr types.str; 27 | description = '' 28 | IPv4 address to use for this domain. 29 | ''; 30 | }; 31 | 32 | ipv4prefix = mkOption { 33 | default = 29; 34 | type = types.int; 35 | description = '' 36 | IPv4 subnet prefix length in bits. 37 | ''; 38 | }; 39 | 40 | ipv6 = mkOption { 41 | default = null; 42 | type = types.nullOr types.str; 43 | description = '' 44 | IPv6 address to use for this domain. 45 | ''; 46 | }; 47 | 48 | ipv6prefix = mkOption { 49 | default = 64; 50 | type = types.int; 51 | description = '' 52 | IPv6 subnet prefix length in bits. 53 | ''; 54 | }; 55 | 56 | device = mkOption { 57 | default = mainDevice; 58 | type = types.str; 59 | description = '' 60 | Network device to assign this virtual host to. By default it's the 61 | device set by . 62 | ''; 63 | }; 64 | 65 | isXMPP = mkOption { 66 | default = false; 67 | type = types.bool; 68 | description = '' 69 | Whether this virtual host is to be used as an XMPP node. 70 | ''; 71 | }; 72 | 73 | ssl = { 74 | domains = mkOption { 75 | type = types.listOf types.str; 76 | default = optional (config.fqdn != null) config.fqdn; 77 | description = '' 78 | Domains for which to get SSL certificates for, defaulting to only 79 | the value specified in if not null. 80 | 81 | The domains specified here are only valid if they share a common 82 | root domain. 83 | ''; 84 | }; 85 | 86 | privateKey = mkOption { 87 | type = types.path; 88 | description = '' 89 | The path to the PEM-encoded private key. 90 | ''; 91 | }; 92 | 93 | certificate = mkOption { 94 | type = types.path; 95 | description = '' 96 | Path to the X.509 public certificate file. 97 | ''; 98 | }; 99 | 100 | chain = mkOption { 101 | type = types.path; 102 | description = '' 103 | Path to the X.509 intermediate chain. 104 | ''; 105 | }; 106 | 107 | fullChain = mkOption { 108 | type = types.path; 109 | description = '' 110 | The and in one 111 | file. 112 | ''; 113 | }; 114 | 115 | allInOne = mkOption { 116 | type = types.path; 117 | description = '' 118 | The and the 119 | in one file. 120 | ''; 121 | }; 122 | }; 123 | }; 124 | 125 | config = mkIf (config.ssl.domains != []) { 126 | ssl.privateKey = ssl.${getRootDomain config}.privkey; 127 | ssl.certificate = ssl.${getRootDomain config}.certificate; 128 | ssl.chain = ssl.${getRootDomain config}.chain; 129 | ssl.fullChain = ssl.${getRootDomain config}.fullchain; 130 | ssl.allInOne = ssl.${getRootDomain config}.full; 131 | }; 132 | }; 133 | 134 | mkNetConfig = name: netcfg: { 135 | ${netcfg.device} = { 136 | ip4 = singleton { 137 | address = netcfg.ipv4; 138 | prefixLength = netcfg.ipv4prefix; 139 | }; 140 | ip6 = singleton { 141 | address = netcfg.ipv6; 142 | prefixLength = netcfg.ipv6prefix; 143 | }; 144 | }; 145 | }; 146 | 147 | netConfig = let 148 | merge = zipAttrsWith (const zipper); 149 | zipper = vals: if isList (head vals) then flatten vals else merge vals; 150 | in merge (mapAttrsToList mkNetConfig cfg.vhosts); 151 | 152 | acmeConfig = let 153 | mkAcmeConfig = const (attrs: { 154 | name = getRootDomain attrs; 155 | value.otherDomains = getOtherDomains attrs; 156 | }); 157 | filterCfg = filterAttrs (const (attrs: attrs.fqdn != null)); 158 | in mapAttrs' mkAcmeConfig (filterCfg cfg.vhosts); 159 | 160 | in { 161 | options.headcounter.vhosts = mkOption { 162 | default = {}; 163 | type = types.attrsOf (types.submodule domainSubmodule); 164 | description = '' 165 | Domains/virtual host configuration. 166 | ''; 167 | }; 168 | 169 | options.headcounter.internalNetConfig = mkOption { 170 | type = types.attrs; 171 | default = {}; 172 | internal = true; 173 | description = '' 174 | An attrset of options from in 175 | order to be passed along to our network simulation module in 176 | ../testing/network.nix. 177 | ''; 178 | }; 179 | 180 | config = mkIf (cfg.vhosts != {}) { 181 | networking.interfaces = netConfig; 182 | headcounter.internalNetConfig = netConfig; 183 | headcounter.services.acme.domains = acmeConfig; 184 | }; 185 | } 186 | -------------------------------------------------------------------------------- /modules/core.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | 3 | { 4 | _module.args.hclib = import ../lib { inherit lib; }; 5 | } 6 | -------------------------------------------------------------------------------- /modules/module-list.nix: -------------------------------------------------------------------------------- 1 | [ 2 | ./core.nix 3 | ./config/conditions.nix 4 | ./config/erlang-inet 5 | ./config/imperative-containers.nix 6 | ./config/ipaddr.nix 7 | ./config/postgresql.nix 8 | ./config/vhosts.nix 9 | ./services/acme 10 | ./services/dyndns 11 | ./services/epmd 12 | ./services/lighttpd.nix 13 | ./services/mongooseim 14 | ./services/mongooseim/ctl.nix 15 | ./services/nix-ssh-build.nix 16 | ./services/nsd-zone-writer 17 | ./services/postfix 18 | ./services/webspace 19 | ] 20 | -------------------------------------------------------------------------------- /modules/services/acme/validator.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric, OverloadedStrings #-} 2 | 3 | import Data.String (IsString(fromString)) 4 | import Control.Monad (void) 5 | import Data.ByteString (ByteString) 6 | import Data.ByteString.Lazy (empty) 7 | import Data.ByteString.Builder (toLazyByteString) 8 | import Data.Serialize (Serialize) 9 | import System.Environment (getArgs, getProgName) 10 | import System.Exit (exitFailure, exitWith, ExitCode(ExitFailure)) 11 | import System.IO (stderr, hPutStrLn) 12 | 13 | import GHC.Generics 14 | 15 | import qualified Data.ByteString.Char8 as BC 16 | 17 | import qualified Nexus.Socket as NS 18 | import qualified Nexus.DNS as DNS 19 | import qualified Nexus.Process as P 20 | 21 | data Command = CmdInstall DNS.FQDN ByteString 22 | | CmdRemove DNS.FQDN 23 | | Sync 24 | deriving (Show, Generic) 25 | 26 | instance Serialize Command 27 | 28 | data ServerConfig = ServerConfig 29 | { updateCmd :: FilePath 30 | , nameservers :: [DNS.FQDN] 31 | } deriving Show 32 | 33 | process :: ServerConfig -> Command -> IO () 34 | process _ Sync = return () 35 | process cfg (CmdInstall fqdn challenge) = 36 | P.pipeToStdin (updateCmd cfg) args zoneData (return ()) onFailure 37 | where 38 | args = [DNS.toByteString $ DNS._zoneDomain zone] 39 | onFailure code = hPutStrLn stderr $ "Zone update for " ++ show fqdn 40 | ++ " failed with exit code " ++ show code 41 | ++ "." 42 | 43 | txtRR = DNS.mkRR $ DNS.TextRecord challenge 44 | nsRRs = DNS.mkRR . DNS.Nameserver . DNS.toRRName <$> nameservers cfg 45 | zone = DNS.mkTempZone acmeZone dummyMail (txtRR : nsRRs) 46 | 47 | acmeZone = mappend fqdn "_acme-challenge" 48 | dummyMail = mappend fqdn "temporary" 49 | zoneData = toLazyByteString $ DNS.renderZone zone 50 | process cfg (CmdRemove fqdn) = 51 | P.pipeToStdin (updateCmd cfg) args empty (return ()) onFailure 52 | where 53 | realFQDN = mappend fqdn "_acme-challenge" 54 | args = ["--delete", DNS.toByteString realFQDN] 55 | onFailure code = hPutStrLn stderr $ "Removal of zone for " ++ show fqdn 56 | ++ " failed with exit code " ++ show code 57 | ++ "." 58 | 59 | server :: ServerConfig -> NS.Connection Command -> IO () 60 | server cfg conn = 61 | maybe (return ()) onData =<< NS.recv conn 62 | where 63 | onData cmd = process cfg cmd >> void (NS.send conn Sync) 64 | 65 | parseOrFail :: String -> (DNS.FQDN -> a) -> Maybe a 66 | parseOrFail d f = either (const Nothing) (Just . f) parsed 67 | where parsed = DNS.parseDomain $ BC.pack d 68 | 69 | parseClientArgs :: [String] -> Maybe Command 70 | parseClientArgs ["challenge-dns-start", fqdn, _, txtValue] = 71 | parseOrFail fqdn $ \f -> CmdInstall f (BC.pack txtValue) 72 | parseClientArgs ["challenge-dns-stop", fqdn, _, _] = 73 | parseOrFail fqdn CmdRemove 74 | parseClientArgs _ = Nothing 75 | 76 | realMain :: [String] -> IO () 77 | realMain ("--server" : cmd : ns) = 78 | NS.serve Nothing . server $ ServerConfig cmd (fromString <$> ns) 79 | realMain ("--client" : host : port : device : rest) = 80 | case parseClientArgs rest of 81 | Nothing -> exitWith (ExitFailure 42) 82 | Just cmd -> NS.connect host port maybeDevice handler 83 | where handler conn = do 84 | True <- NS.send conn cmd 85 | Just Sync <- NS.recv conn 86 | return () 87 | where 88 | maybeDevice = if device == "" then Nothing else Just device 89 | realMain _ = do 90 | prog <- getProgName 91 | hPutStrLn stderr $ "Usage: " ++ prog ++ " --server COMMAND" 92 | hPutStrLn stderr $ " " ++ prog ++ " --client HOST PORT COMMAND..." 93 | exitFailure 94 | 95 | main :: IO () 96 | main = getArgs >>= realMain 97 | -------------------------------------------------------------------------------- /modules/services/epmd/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | cfg = config.headcounter.services.epmd; 7 | 8 | epmdPatched = lib.overrideDerivation pkgs.erlang (o: { 9 | name = "epmd-${o.version}"; 10 | 11 | NIX_CFLAGS_COMPILE = "-I${pkgs.systemd.dev}/include"; 12 | NIX_LDFLAGS = "-L${pkgs.systemd.lib}/lib"; 13 | 14 | configureFlags = o.configureFlags ++ [ "--enable-systemd" ]; 15 | 16 | preBuild = "export ERL_TOP=\"$(pwd)\""; 17 | buildFlags = [ "-C erts/epmd/src" ]; 18 | 19 | installPhase = "install -vD bin/*/epmd \"$out/bin/epmd\""; 20 | 21 | postFixup = null; 22 | }); 23 | 24 | sockets = listToAttrs (map (addr: { 25 | name = "epmd-${replaceChars ["."] ["-"] addr}"; 26 | value = { 27 | description = "Socket for EPMD address ${addr}"; 28 | wantedBy = [ "sockets.target" ]; 29 | socketConfig.ListenStream = "${addr}:${toString cfg.port}"; 30 | socketConfig.Service = "epmd.service"; 31 | }; 32 | }) cfg.addresses); 33 | 34 | in { 35 | options.headcounter.services.epmd = { 36 | enable = mkEnableOption "Erlang port mapper daemon"; 37 | 38 | port = mkOption { 39 | type = types.int; 40 | default = 4369; 41 | description = '' 42 | The TCP port to listen for incoming name queries. 43 | ''; 44 | }; 45 | 46 | addresses = mkOption { 47 | type = types.listOf types.str; 48 | default = [ "127.0.0.1" ]; 49 | description = '' 50 | Addresses to listen for incoming name queries. 51 | ''; 52 | }; 53 | }; 54 | 55 | config = mkIf cfg.enable { 56 | users.extraGroups.epmd = {}; 57 | users.extraUsers.epmd = { 58 | description = "Erland Port Mapper User"; 59 | group = "epmd"; 60 | }; 61 | 62 | systemd.sockets = sockets; 63 | 64 | systemd.services.epmd = { 65 | description = "Erlang Port Mapper Daemon"; 66 | serviceConfig.ExecStart = toString [ 67 | "@${epmdPatched}/bin/epmd" "epmd" "-debug" "-systemd" 68 | ]; 69 | serviceConfig.Sockets = map (s: "${s}.socket") (attrNames sockets); 70 | serviceConfig.User = "epmd"; 71 | serviceConfig.Group = "epmd"; 72 | }; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /modules/services/mongooseim/access.nix: -------------------------------------------------------------------------------- 1 | { type, optPrefix, patterns }: 2 | 3 | { config, lib, ... }: 4 | 5 | let 6 | checkPattern = passthru: let 7 | pattern = config.match; 8 | isValid = pattern == null || patterns ? ${pattern}; 9 | patOpt = "${optPrefix}.acl.patterns"; 10 | err = "No pattern `${pattern}' defined in `${patOpt}'."; 11 | in if isValid then passthru else throw err; 12 | 13 | mkResult = resType: { 14 | tuple = [ 15 | resType 16 | { atom = if config.match == null then "all" else config.match; } 17 | ]; 18 | }; 19 | 20 | optsAndResults = if type == "shaper" then { 21 | options.shaper = lib.mkOption { 22 | type = lib.types.nullOr lib.types.str; 23 | description = '' 24 | A shaper name defined by , where 25 | its rate value is returned on a successful . 26 | 27 | If the value is null, no shaping is done for the 28 | operation. 29 | ''; 30 | }; 31 | result = mkResult { 32 | atom = if config.shaper == null then "none" else config.shaper; 33 | }; 34 | } else if type == "limit" then { 35 | options.limit = lib.mkOption { 36 | type = lib.types.nullOr lib.types.int; 37 | description = '' 38 | A limit as an integer or null for no limit, where the 39 | limit is returned on a successful . 40 | ''; 41 | }; 42 | result = mkResult (if config.limit == null then { 43 | atom = "infinity"; 44 | } else config.limit); 45 | } else if type == "access" then { 46 | options.allow = lib.mkOption { 47 | type = lib.types.bool; 48 | description = '' 49 | Whether to allow (true) or deny 50 | (false) an operation on a successful 51 | . 52 | ''; 53 | }; 54 | result = mkResult { 55 | atom = if config.allow then "allow" else "deny"; 56 | }; 57 | } else throw "Invalid access submodule type ${type}."; 58 | 59 | in { 60 | options = { 61 | match = lib.mkOption { 62 | type = lib.types.nullOr lib.types.str; 63 | default = null; 64 | description = '' 65 | Match an ACL pattern defined by 66 | or null 67 | to match on all JIDs. 68 | ''; 69 | }; 70 | 71 | expression = lib.mkOption { 72 | type = lib.types.attrs; 73 | internal = true; 74 | description = '' 75 | The resulting Nix expression that's translated to an Erlang expression 76 | for the options given by this submodule. 77 | ''; 78 | }; 79 | } // optsAndResults.options; 80 | 81 | config.expression = checkPattern optsAndResults.result; 82 | } 83 | -------------------------------------------------------------------------------- /modules/services/mongooseim/acl.nix: -------------------------------------------------------------------------------- 1 | { options, config, lib, ... }: 2 | 3 | let 4 | matchType = lib.mkOptionType { 5 | name = "string, `{ regex = string; }' or `{ glob = string; }'"; 6 | check = m: let 7 | isOnly = attr: lib.attrNames m == [ attr ]; 8 | in lib.isString m || (lib.isAttrs m && (isOnly "regex" || isOnly "glob")); 9 | merge = lib.mergeOneOption; 10 | }; 11 | 12 | mkAclOption = what: lib.mkOption { 13 | type = matchType; 14 | example.regex = "^[0-9]+$"; 15 | description = '' 16 | Match a specific ${what}. 17 | 18 | 19 | If the value is a plain string, an exact match is done 20 | against the ${what}. 21 | 22 | If the value is an attribute set like 23 | { regex = "SOME_REGEX"; } the ${what} is matched 24 | against a regular expression. The syntax of this regular expression is 25 | PCRE with a few Erlang-specific differences, see . 27 | 28 | 29 | If the value is an attribute set like 30 | { glob = "SOME_REGEX"; } the ${what} is matched 31 | using shell glob wildcards like described in 32 | glob 33 | 7 34 | . 35 | 36 | ''; 37 | }; 38 | 39 | optAttrs = [ "user" "server" "resource" ]; 40 | genOptAttrs = lib.genAttrs optAttrs; 41 | 42 | genDefined = opt: 43 | if !options.${opt}.isDefined then null 44 | else if config.${opt} ? regex then "regex" 45 | else if config.${opt} ? glob then "glob" 46 | else "plain"; 47 | 48 | definedOptions = lib.genAttrs optAttrs genDefined; 49 | 50 | transform = transfs: let 51 | current = lib.head transfs; 52 | pprintDef = { opt, val }: 53 | if val ? regex then "${opt}.regex = `${val.regex}'" 54 | else if val ? glob then "${opt}.glob = `${val.glob}'" 55 | else "${opt} = `${val}'"; 56 | defs = lib.fold (opt: acc: let 57 | val = config.${opt}; 58 | inherit (options.${opt}) isDefined; 59 | in acc ++ lib.optional isDefined { inherit opt val; }) [] optAttrs; 60 | what = lib.concatMapStringsSep " and " pprintDef defs; 61 | notFound = "Unable to find a valid MongooseIM ACL match rule for ${what}."; 62 | in if transfs == [] then throw notFound 63 | else if current.matches == definedOptions then current.result 64 | else transform (lib.tail transfs); 65 | 66 | mkRule = resultAtom: transfs: let 67 | lookupTrans = trans: 68 | if lib.isList trans then lib.getAttrFromPath trans config 69 | else config.${trans}; 70 | 71 | in { 72 | matches = let 73 | nullAttrs = lib.genAttrs optAttrs (lib.const null); 74 | transAttrs = map (path: { 75 | name = if lib.isList path then lib.head path else path; 76 | value = if lib.isList path then lib.last path else "plain"; 77 | }) transfs; 78 | in nullAttrs // lib.listToAttrs transAttrs; 79 | 80 | result = { 81 | tuple = lib.singleton { 82 | atom = resultAtom; 83 | } ++ map lookupTrans transfs; 84 | }; 85 | }; 86 | 87 | transformed = transform [ 88 | (mkRule "user" [ "user" ]) 89 | (mkRule "user" [ "user" "server" ]) 90 | (mkRule "server" [ "server" ]) 91 | (mkRule "resource" [ "resource" ]) 92 | (mkRule "user_regexp" [ ["user" "regex"] ]) 93 | (mkRule "user_regexp" [ ["user" "regex"] "server" ]) 94 | (mkRule "server_regexp" [ ["server" "regex"] ]) 95 | (mkRule "resource_regexp" [ ["resource" "regex"] ]) 96 | (mkRule "node_regexp" [ ["user" "regex"] ["server" "regex"] ]) 97 | (mkRule "user_glob" [ ["user" "glob"] ]) 98 | (mkRule "user_glob" [ ["user" "glob"] "server" ]) 99 | (mkRule "server_glob" [ ["server" "glob"] ]) 100 | (mkRule "resource_glob" [ ["resource" "glob"] ]) 101 | (mkRule "node_glob" [ ["user" "glob"] ["server" "glob"] ]) 102 | ]; 103 | 104 | in { 105 | options = { 106 | user = mkAclOption "user name"; 107 | server = mkAclOption "host name"; 108 | resource = mkAclOption "resource"; 109 | all = lib.mkOption { 110 | type = lib.types.bool; 111 | default = false; 112 | description = '' 113 | Match on everything if true and overrides all of the 114 | other matches. 115 | ''; 116 | }; 117 | 118 | match = lib.mkOption { 119 | type = lib.types.nullOr lib.types.attrs; 120 | internal = true; 121 | description = '' 122 | The resulting Nix expression that's translated to an Erlang expression 123 | for the options given by one of the match options. 124 | ''; 125 | }; 126 | }; 127 | 128 | config.match = let 129 | nothing = lib.all (o: !options.${o}.isDefined) optAttrs && (!config.all); 130 | something = if config.all then { atom = "all"; } else transformed; 131 | in if nothing then null else something; 132 | } 133 | -------------------------------------------------------------------------------- /modules/services/mongooseim/ctl.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, hclib, config, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | cfg = config.headcounter.programs.mongooseimctl; 7 | inherit (config.headcounter.services.mongooseim) package; 8 | 9 | mimlib = import ./lib.nix { inherit config hclib lib; }; 10 | 11 | ctlArgsFile = pkgs.writeText "ctl.args" '' 12 | -setcookie ${hclib.shErlEsc hclib.erlAtom cfg.cookie} 13 | ${mimlib.loopbackArg} 14 | ${mimlib.inetArg} 15 | -pa ${hclib.shErlEsc id "${package.mainAppDir}/ebin"} 16 | ''; 17 | 18 | mongooseimctl = pkgs.writeScriptBin "mongooseimctl" '' 19 | #!${pkgs.stdenv.shell} 20 | if [ "$1" = debug ]; then 21 | shift 22 | # If TERM is unset or "dumb" -remsh doesn't connect to the remote node. 23 | [ -z "$TERM" -o "$TERM" = dumb ] && export TERM=vt100 24 | exec ${pkgs.erlang}/bin/erl \ 25 | -args_file ${lib.escapeShellArg ctlArgsFile} \ 26 | -sname "ctl$$@"${lib.escapeShellArg cfg.ctlHost} \ 27 | -hidden -remsh ${lib.escapeShellArg cfg.destNodeName} "$@" 28 | fi 29 | ${pkgs.erlang}/bin/erl \ 30 | -args_file ${lib.escapeShellArg ctlArgsFile} \ 31 | -sname "ctl$$@"${lib.escapeShellArg cfg.ctlHost} \ 32 | -noinput -hidden -s ejabberd_ctl \ 33 | -extra ${lib.escapeShellArg cfg.destNodeName} "$@" 34 | retcode=$? 35 | if [ $retcode -eq 2 -o $retcode -eq 3 ]; then 36 | echo 37 | echo "Use \`mongooseimctl debug' to enter an interactive Erlang shell" \ 38 | "of a running MongooseIM node." 39 | fi 40 | exit $retcode 41 | ''; 42 | 43 | in { 44 | options.headcounter.programs.mongooseimctl = { 45 | enable = mkEnableOption "MongooseIM controller"; 46 | 47 | ctlHost = mkOption { 48 | default = config.networking.hostName; 49 | type = types.str; 50 | description = '' 51 | The host part of the node name to use for spawning the client side of 52 | the controller. A random node name will be generated at runtime. 53 | ''; 54 | }; 55 | 56 | destNodeName = mkOption { 57 | default = "mongooseim@${config.networking.hostName}"; 58 | type = types.str; 59 | description = '' 60 | Erlang node name of the MongooseIM server to control. 61 | ''; 62 | }; 63 | 64 | cookie = mkOption { 65 | example = "super_secret_random_sequence"; 66 | type = types.str; 67 | description = '' 68 | The magic cookie used for communicating with the MongooseIM Erlang node. 69 | ''; 70 | }; 71 | 72 | ctlTool = mkOption { 73 | type = types.path; 74 | internal = true; 75 | description = '' 76 | The full path to the mongooseimctl tool, generated by 77 | this very module. 78 | ''; 79 | }; 80 | }; 81 | 82 | config = mkIf cfg.enable { 83 | environment.systemPackages = singleton mongooseimctl; 84 | headcounter.programs.mongooseimctl = { 85 | ctlTool = "${mongooseimctl}/bin/mongooseimctl"; 86 | }; 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /modules/services/mongooseim/lib.nix: -------------------------------------------------------------------------------- 1 | { config, hclib, lib, ... }: 2 | 3 | let 4 | epmdAddresses = config.headcounter.services.epmd.addresses; 5 | isLoopbackEpmd = epmdAddresses == lib.singleton "127.0.0.1"; 6 | distUseLoopback = "-kernel inet_dist_use_interface '{127, 0, 0, 1}'"; 7 | 8 | mimcfg = config.headcounter.services.mongooseim; 9 | needsInet = mimcfg.enable && mimcfg.nodeIp != null; 10 | inherit (config.headcounter.erlang-inet) inetConfigFile; 11 | inherit (hclib) shErlEsc erlString; 12 | kernelInetrc = "-kernel inetrc ${shErlEsc erlString inetConfigFile}"; 13 | 14 | in { 15 | loopbackArg = lib.optionalString isLoopbackEpmd distUseLoopback; 16 | inetArg = lib.optionalString needsInet kernelInetrc; 17 | } 18 | -------------------------------------------------------------------------------- /modules/services/mongooseim/listeners.nix: -------------------------------------------------------------------------------- 1 | { pkgs, hclib, ... }: 2 | 3 | { lib, config, ... }: 4 | 5 | let 6 | inherit (lib) mkOption types; 7 | 8 | cowboyDoc = "https://ninenines.eu/docs/en/cowboy/1.0/guide"; 9 | 10 | httpModule.options = { 11 | host = mkOption { 12 | type = types.nullOr types.str; 13 | default = null; 14 | example = "example.org"; 15 | description = '' 16 | The virtual host name to serve requests for the associated 17 | . 18 | 19 | If the value is null any host is accepted. 20 | 21 | This allows to use the host match syntax of 23 | the Cowboy web server. 24 | ''; 25 | }; 26 | 27 | path = mkOption { 28 | type = types.str; 29 | default = "/"; 30 | example = "/api/rooms/:id/users/[:user]"; 31 | description = let 32 | in '' 33 | The path part of the URL to map to the associated 34 | . 35 | 36 | This allows to use the path match syntax of 38 | the Cowboy web server. 39 | ''; 40 | }; 41 | 42 | handler = mkOption { 43 | type = types.str; 44 | example = "mod_bosh"; 45 | description = '' 46 | The handler module to use for serving requests, which is any Cowboy 47 | module, not only the ones that come with MongooseIM. 48 | 49 | Handlers that come with the MongooseIM source tree: 50 | 51 | 52 | ${lib.concatMapStrings (handler: '' 53 | ${handler} 54 | '') [ 55 | /* Retrieved using the following command on the source tree: 56 | 57 | grep 'cowboy\|^routes' apps/ejabberd/src/*.erl | sed -r \ 58 | -e '/\/((ejabberd|mod)_cowboy|mongoose_api(_common)?)\.erl:/d' \ 59 | -e '/^[^:]*: *%/d' -e 's/^.*\/([^\/]+)\.erl:.*$/"\1"/' | sort -u 60 | */ 61 | "mod_bosh" 62 | "mod_revproxy" 63 | "mod_websockets" 64 | "mongoose_api_admin" 65 | "mongoose_api_client" 66 | "mongoose_api_metrics" 67 | "mongoose_api_users" 68 | "mongoose_client_api" 69 | "mongoose_client_api_messages" 70 | "mongoose_client_api_rooms" 71 | "mongoose_client_api_rooms_messages" 72 | "mongoose_client_api_rooms_users" 73 | ]} 74 | 75 | ''; 76 | }; 77 | 78 | options = mkOption { 79 | type = hclib.types.erlPropList; 80 | default = {}; 81 | example.mongoose_client_api_sse.flag = true; 82 | description = '' 83 | Additional options to provide to the . 84 | ''; 85 | }; 86 | }; 87 | 88 | in { 89 | options = { 90 | port = mkOption { 91 | type = types.int; 92 | description = "The port to listen on."; 93 | }; 94 | 95 | address = mkOption { 96 | type = types.nullOr types.str; 97 | default = null; 98 | example = "2001:6f8:900:72a::2"; 99 | description = "The IPv4 or IPv6 address to listen on."; 100 | }; 101 | 102 | type = mkOption { 103 | type = types.nullOr types.str; 104 | default = null; 105 | example = "ws"; 106 | description = "The service type to use for this listener"; 107 | }; 108 | 109 | module = mkOption { 110 | type = types.str; 111 | example = "ejabberd_c2s"; 112 | description = "The Ejabberd module to use for this service."; 113 | }; 114 | 115 | options = mkOption { 116 | type = hclib.types.erlPropList; 117 | default = {}; 118 | example = { 119 | access.atom = "trusted_users"; 120 | shaper_rule = "fast"; 121 | }; 122 | description = "Options for the corresponding ."; 123 | }; 124 | 125 | http = { 126 | enable = mkOption { 127 | type = types.bool; 128 | default = false; 129 | example = true; 130 | description = '' 131 | Whether this listener module should use 132 | ejabberd_cowboy for HTTP(S). 133 | ''; 134 | }; 135 | 136 | modules = mkOption { 137 | type = types.listOf (types.submodule httpModule); 138 | default = []; 139 | example = [ 140 | { host = "localhost"; 141 | path = "/api"; 142 | handler = "mongoose_api_admin"; 143 | } 144 | { path = "/api"; 145 | handler = "lasse_handler"; 146 | options.mongoose_client_api_sse.flag = true; 147 | } 148 | ]; 149 | description = '' 150 | A list of HTTP(S) modules (using ejabberd_cowboy) 151 | to enable for this listener. 152 | ''; 153 | }; 154 | }; 155 | 156 | expression = lib.mkOption { 157 | type = lib.types.attrs; 158 | internal = true; 159 | description = '' 160 | The resulting Nix expression that's translated to an Erlang expression 161 | for the options given by this submodule. 162 | ''; 163 | }; 164 | }; 165 | 166 | config = lib.mkMerge [ 167 | (lib.mkIf config.http.enable { 168 | module = "ejabberd_cowboy"; 169 | options.modules = map (module: { 170 | tuple = [ 171 | (if module.host == null then "_" else module.host) 172 | module.path 173 | { atom = module.handler; } 174 | { __raw = module.options; } 175 | ]; 176 | }) config.http.modules; 177 | }) 178 | { expression = let 179 | inherit (hclib) parseErlIpAddr erlInt erlAtom; 180 | addrTerm = parseErlIpAddr config.address; 181 | addrSpec = lib.singleton config.port 182 | ++ lib.optional (config.address != null) { __raw = addrTerm; } 183 | ++ lib.optional (config.type != null) { atom = config.type; }; 184 | addr = if lib.length addrSpec == 1 then lib.head addrSpec 185 | else { tuple = addrSpec; }; 186 | in { 187 | tuple = [ addr { atom = config.module; } { __raw = config.options; } ]; 188 | }; 189 | } 190 | ]; 191 | } 192 | -------------------------------------------------------------------------------- /modules/services/nix-ssh-build.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | with lib; 4 | 5 | { 6 | options.headcounter.nix.sshBuild = { 7 | enable = mkEnableOption "building using SSH"; 8 | keys = mkOption { 9 | type = types.listOf types.str; 10 | default = []; 11 | example = [ "ssh-dss AAAAB3NzaC1k... alice@example.org" ]; 12 | description = "A list of SSH public keys allowed build via SSH."; 13 | }; 14 | }; 15 | 16 | config = mkIf config.headcounter.nix.sshBuild.enable { 17 | services.openssh.enable = true; 18 | 19 | services.openssh.extraConfig = '' 20 | Match User nix-remote-build 21 | AllowAgentForwarding no 22 | AllowTcpForwarding no 23 | PermitTTY no 24 | PermitTunnel no 25 | X11Forwarding no 26 | ForceCommand ${config.nix.package}/bin/nix-store --serve --write 27 | Match All 28 | ''; 29 | 30 | users.extraUsers.nix-remote-build = { 31 | description = "Nix SSH build user"; 32 | uid = 412; 33 | useDefaultShell = true; 34 | openssh.authorizedKeys.keys = config.headcounter.nix.sshBuild.keys; 35 | }; 36 | 37 | nix.trustedUsers = [ "nix-remote-build" ]; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /modules/services/nsd-zone-writer/writer.sh.in: -------------------------------------------------------------------------------- 1 | if [ $# -eq 2 -a "$1" = "--delete" ]; then 2 | fqdn="$2" 3 | delete=1 4 | else 5 | fqdn="$1" 6 | delete=0 7 | fi 8 | 9 | umask 0027 10 | zonefile="@baseDir@/$fqdn.zone" 11 | 12 | touchZonefile() { 13 | @touch@ -r "$zonefile" -d '1 sec' "$zonefile" 14 | } 15 | 16 | if [ -e "$zonefile" ]; then 17 | oldMTime="$(@stat@ -c %Y "$zonefile")" 18 | exists=1 19 | else 20 | oldMTime=0 21 | exists=0 22 | fi 23 | 24 | if [ $delete -eq 1 ]; then 25 | if [ $exists -eq 1 ]; then 26 | @ctrl@ delzone "$fqdn" 27 | @rm@ -f "$zonefile" 28 | exit 0 29 | else 30 | echo "Zone file $zonefile doesn't exist." >&2 31 | exit 1 32 | fi 33 | fi 34 | 35 | zonedata="$(@cat@)" 36 | 37 | if ! zoneErrors="$(echo "$zonedata" | @checkzone@ "$fqdn" - 2>&1)"; then 38 | echo "$zoneErrors" >&2 39 | exit 1 40 | fi 41 | 42 | echo "$zonedata" > "$zonefile" 43 | 44 | if [ "$oldMTime" -eq "$(@stat@ -c %Y "$zonefile")" ]; then 45 | touchZonefile 46 | fi 47 | 48 | coproc waitForUpdate { 49 | @inotifywait@ --format %w -m -e close "$zonefile" 2>&1 50 | } 51 | 52 | watching=0 53 | 54 | while read line <&${waitForUpdate[0]}; do 55 | if [ "x$line" = "xWatches established." ]; then 56 | watching=1 57 | break 58 | fi 59 | done 60 | 61 | if [ $watching -eq 0 ]; then 62 | kill -TERM %% &> /dev/null || : 63 | echo "Could not establish inotify watch for $zonefile!" >&2 64 | exit 1 65 | fi 66 | 67 | if [ $exists -eq 1 ]; then 68 | echo -n "Reloading zone $fqdn: " >&2 69 | @ctrl@ reload "$fqdn" >&2 70 | else 71 | @ctrl@ addzone "$fqdn" dynzone 72 | fi 73 | 74 | for waitTime in 1 2 5 10 30; do 75 | if read -t $waitTime line <&${waitForUpdate[0]}; then 76 | if [ "x$line" = "x$zonefile" ]; then 77 | kill -TERM %% &> /dev/null || : 78 | wait &> /dev/null || : 79 | echo "Reload of $fqdn successful." >&2 80 | exit 0 81 | fi 82 | fi 83 | echo "Reload of $fqdn failed, touching zone file and" \ 84 | "resending reload..." >&2 85 | touchZonefile 86 | read touched <&${waitForUpdate[0]} 87 | @ctrl@ reload "$fqdn" >&2 88 | done 89 | echo "Reloading of zone $fqdn failed after 5 retries." >&2 90 | exit 1 91 | -------------------------------------------------------------------------------- /modules/services/postfix/postpatch.nix: -------------------------------------------------------------------------------- 1 | { lib, writeText, postfix, systemd }: 2 | 3 | let 4 | postSed = writeText "postfix-single-config.sed" '' 5 | # The mail_conf_suck() function is for reading the Postfix main.cf based on 6 | # CONF_ENV_DIR. We want it to read a single file instead of assuming that 7 | # everything will be in a specific directory. 8 | /void.*\/ { 60 | # We now have the full function in the pattern space, so we can do 61 | # replacements accross multiple lines. 62 | s,%s/%s,%s,g 63 | s/\,[^,)]*/var_config_file/g 64 | } 65 | bo 66 | } 67 | blm 68 | } 69 | 70 | :o # Miscellaneous replacements that only span single words/lines 71 | 72 | # Lines like this one: 73 | # 74 | # path = concatenate(DEF_CONFIG_DIR, "/", "main.cf", (char *) 0); 75 | # 76 | # ... need to be turned into this: 77 | # 78 | # patch = mystrdup(var_config_file); 79 | s/concatenate(.*\.*MAIN.*);/mystrdup(var_config_file);/g 80 | 81 | # Avoid reporting a single config file in plural form. 82 | s/using config files in/using config file/g 83 | 84 | # Every other var_config_dir needs to be replaced as well, including its 85 | # declaration. This might be too generic but we still have the compiler as 86 | # a safety net. 87 | s/\/var_config_file/g 88 | ''; 89 | 90 | in lib.overrideDerivation postfix (drv: { 91 | NIX_CFLAGS_COMPILE = lib.flatten [ 92 | (drv.NIX_CFLAGS_COMPILE or []) 93 | "-I${systemd.dev or systemd}/include" 94 | ]; 95 | 96 | postPatch = (drv.postPatch or "") + '' 97 | # Run the sed expression over all files except for postconf. 98 | # 99 | # The reason we don't want to replace postconf stuff as well is because it 100 | # tries to modify not only main.cf but also master.cf. 101 | # 102 | # We don't use master.cf and we also don't use postconf (our config files 103 | # are immutable anyway), so we just make sure that it compiles by adding a 104 | # declaration for var_config_dir to the postconf header file, because we 105 | # have replaced var_config_dir with var_config_file in mail_params.h 106 | # already. 107 | find . -path ./src/postconf -prune -o \ 108 | -type f -exec sed -i -f "${postSed}" {} + 109 | echo "char *var_config_dir;" >> src/postconf/postconf.h 110 | 111 | # Prevent services from picking up MASTER_STATUS_FD, because it clashes with 112 | # systemd-provided FDs. 113 | sed -i -e '/MASTER_STATUS_FD/d' src/master/*_server.c 114 | 115 | # Use the constant defined by SD_LISTEN_FDS_START as the MASTER_LISTEN_FD. 116 | # In theory we could just use 3 here, but to make sure this doesn't change 117 | # in newer systemd versions, let's use the constant from "sd-daemon.h". 118 | sed -i -e '/^#define.*MASTER_LISTEN_FD/ { 119 | c #include \ 120 | #define MASTER_LISTEN_FD SD_LISTEN_FDS_START 121 | }' src/master/master_proto.h 122 | 123 | # Don't try to read global config file during postmap. 124 | sed -i -e '/mail_conf_read/,/mail_dict_init/d' src/postmap/postmap.c 125 | 126 | # Make master_notify a no-op, because systemd is already handling child 127 | # process limiting. 128 | sed -i -e '/int *master_notify/ { 129 | n # Skip function definition 130 | /^{/!b # Make sure the definition starts with a { on its own line 131 | :l; N; /\n}/!bl # Loop until end of function body 132 | s/.*/{ return 0; }/ # Simply return success 133 | }' src/master/master_proto.c 134 | ''; 135 | }) 136 | -------------------------------------------------------------------------------- /modules/services/webspace/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, config, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | cfg = config.headcounter.services.webspace; 7 | 8 | openssh = lib.overrideDerivation pkgs.openssh (o: { 9 | patches = (o.patches or []) ++ singleton ./openssh.patch; 10 | }); 11 | 12 | basedir = "/var/www"; 13 | group = config.headcounter.services.lighttpd.group; 14 | 15 | sftpdConfigFile = pkgs.writeText "sftpd.config" '' 16 | Protocol 2 17 | Port 2222 18 | PermitRootLogin no 19 | ChrootDirectory ${basedir} 20 | Subsystem sftp ${openssh}/libexec/sftp-server 21 | AllowAgentForwarding no 22 | PermitTTY no 23 | PermitTunnel no 24 | AllowTcpForwarding no 25 | X11Forwarding no 26 | UsePAM no 27 | UsePrivilegeSeparation sandbox 28 | 29 | ${flip concatMapStrings config.services.openssh.hostKeys (k: '' 30 | HostKey ${k.path} 31 | '')} 32 | 33 | AllowUsers ${concatStringsSep " " ( 34 | map (n: "webspace_${n}") (attrNames cfg.accounts) 35 | )} 36 | 37 | ${concatStrings (mapAttrsToList (name: opts: '' 38 | Match User webspace_${name} 39 | ForceCommand internal-sftp -d ${name} -u 027 40 | '') cfg.accounts)} 41 | ''; 42 | in { 43 | options.headcounter.services.webspace = { 44 | enable = mkEnableOption "headcounter webspace"; 45 | 46 | accounts = mkOption { 47 | default = {}; 48 | example = { 49 | funnyuser = { 50 | uid = 1000; 51 | userOptions = { 52 | password = "foobar"; 53 | }; 54 | virtualHosts = [ 55 | { type = "regex"; 56 | on = "^(?:[a-z]+\\.)?example\\.com\$"; 57 | } 58 | { type = "static"; 59 | on = "example.org"; 60 | } 61 | ]; 62 | }; 63 | }; 64 | type = types.attrsOf (types.submodule { 65 | options.uid = mkOption { 66 | type = types.uniq (mkOptionType { 67 | name = "int between 1000 and 5000"; 68 | check = uid: isInt uid && uid >= 1000 && uid <= 5000; 69 | merge = mergeOneOption; 70 | }); 71 | example = 1000; 72 | description = '' 73 | User ID for the webspace user, has to be between 1000 and 5000. 74 | ''; 75 | }; 76 | 77 | options.userOptions = mkOption { 78 | default = {}; 79 | example = { 80 | password = "abcde"; 81 | extraGroups = "mail"; 82 | }; 83 | type = types.attrs; 84 | description = '' 85 | Extra user options that will be merged with 86 | . 87 | 88 | Note that the attributes , 89 | , and 90 | can't be set here. 91 | ''; 92 | }; 93 | 94 | options.virtualHosts = mkOption { 95 | default = []; 96 | example = singleton { 97 | type = "static"; 98 | on = "www.example.com"; 99 | }; 100 | type = types.listOf types.attrs; 101 | description = '' 102 | Virtual hosts to be routed to this webspace account. 103 | The options are the same as 104 | . 105 | ''; 106 | }; 107 | }); 108 | description = '' 109 | Webspace account and virtual host definitions. All 110 | are merged with values of 111 | and are 112 | merged with . 113 | ''; 114 | }; 115 | }; 116 | 117 | config = mkIf (cfg.enable && cfg.accounts != {}) { 118 | headcounter.services.lighttpd = { 119 | enable = true; 120 | 121 | configuration = '' 122 | mimetype.assign = ( 123 | ".gif" => "image/gif", 124 | ".jpg" => "image/jpeg", 125 | ".jpeg" => "image/jpeg", 126 | ".png" => "image/png", 127 | ".css" => "text/css", 128 | ".html" => "text/html", 129 | ".htm" => "text/html", 130 | ".js" => "text/javascript", 131 | "" => "application/octet-stream" 132 | ) 133 | ''; 134 | 135 | virtualHosts = flatten (mapAttrsToList (name: opts: map (vhost: { 136 | socket = ":80"; 137 | docroot = "${basedir}/${name}"; 138 | } // vhost) opts.virtualHosts) cfg.accounts); 139 | }; 140 | 141 | systemd.services.sftpd = { 142 | description = "SFTP Service"; 143 | after = [ "sshd.service" ]; 144 | wantedBy = [ "multi-user.target" ]; 145 | 146 | serviceConfig = { 147 | ExecStart = "${openssh}/bin/sshd -f \"${sftpdConfigFile}\""; 148 | KillMode = "process"; 149 | Restart = "always"; 150 | }; 151 | }; 152 | 153 | users.extraUsers = mapAttrs' (name: opts: { 154 | name = "webspace_${name}"; 155 | value = opts.userOptions // { 156 | inherit (opts) uid; 157 | home = "${basedir}/${name}"; 158 | inherit group; 159 | createHome = false; 160 | }; 161 | }) cfg.accounts; 162 | 163 | system.activationScripts.createWebroots = let 164 | scripts = mapAttrsToList (name: opts: '' 165 | umask 066 && mkdir -p "${basedir}/${name}" 166 | chmod 2710 "${basedir}/${name}" 167 | chown "webspace_${name}:${group}" "${basedir}/${name}" 168 | '') cfg.accounts; 169 | in stringAfter [ "etc" "users" "groups" ] (concatStrings scripts); 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /modules/services/webspace/openssh.patch: -------------------------------------------------------------------------------- 1 | diff --git a/auth2.c b/auth2.c 2 | index a5490c0..e33a627 100644 3 | --- a/auth2.c 4 | +++ b/auth2.c 5 | @@ -216,11 +216,18 @@ input_userauth_request(int type, u_int32_t seq, void *ctxt) 6 | Authmethod *m = NULL; 7 | char *user, *service, *method, *style = NULL; 8 | int authenticated = 0; 9 | + char *buf; 10 | + u_int ulen; 11 | 12 | if (authctxt == NULL) 13 | fatal("input_userauth_request: no authctxt"); 14 | 15 | - user = packet_get_cstring(NULL); 16 | + user = packet_get_cstring(&ulen); 17 | + buf = xmalloc(ulen + 10); 18 | + snprintf(buf, ulen + 10, "webspace_%s", user); 19 | + free(user); 20 | + user = buf; 21 | + 22 | service = packet_get_cstring(NULL); 23 | method = packet_get_cstring(NULL); 24 | debug("userauth-request for user %s service %s method %s", user, service, method); 25 | -------------------------------------------------------------------------------- /modules/testing/network.nix: -------------------------------------------------------------------------------- 1 | # Support module which should simulate a virtual Internet-like environment. 2 | # 3 | # This doesn't involve adding any routers but instead rewrites the network 4 | # interface address configuration for the test nodes. 5 | # 6 | # XXX: Right now this module isn't pluggable and heavily depends on 7 | # Headcounter-internal options (search for "config.headcounter"), because 8 | # we can't simply replace the interface options created by the "ip-address" 9 | # module key in 17.03. 10 | # 11 | # However, NixOS unstable (and upcoming 17.09) has a "disabledModules" 12 | # attribute which we can use to replace the "ip-address" module. 13 | # 14 | { nodes, config, lib, ... }: 15 | 16 | let 17 | # We do our address assignment by ourselves, because we want to have a more 18 | # unrestricted prefix length in our network. 19 | forceSingleton = x: lib.mkForce (lib.singleton x); 20 | maybeForce = cond: x: lib.optionals (cond == null) (forceSingleton x); 21 | getIndex = e: list: 22 | if list == [] then throw "Node index for ${e} not found!" 23 | else if lib.head list == e then lib.length list 24 | else getIndex e (lib.tail list); 25 | index = getIndex config.networking.hostName (lib.attrNames nodes); 26 | 27 | # TODO: Move into lib/ maybe? 28 | hexChars = lib.stringToCharacters "0123456789abcdef"; 29 | dec2hex = x: let 30 | d = x / 16; 31 | r = x - d * 16; 32 | hex = builtins.elemAt hexChars r; 33 | in if d == 0 then hex else dec2hex d + hex; 34 | 35 | # Either use the IP address provided in opt or if that is null, use fallback. 36 | mkAddr = opt: fallback: 37 | if config.headcounter.${opt} != null 38 | then config.headcounter.${opt} 39 | else fallback; 40 | 41 | mainIPv4.address = mkAddr "mainIPv4" "93.184.216.${toString index}"; 42 | mainIPv4.prefixLength = 0; 43 | mainIPv6.address = mkAddr "mainIPv6" "fdbc::${dec2hex index}"; 44 | mainIPv6.prefixLength = 0; 45 | 46 | vhostCfg = let 47 | inherit (config.headcounter) internalNetConfig mainDevice; 48 | in internalNetConfig.${mainDevice} or { ip4 = []; ip6 = []; }; 49 | 50 | in { 51 | virtualisation.vlans = lib.mkForce [ 1 ]; 52 | headcounter.mainDevice = "eth1"; 53 | 54 | networking.useDHCP = false; 55 | networking.primaryIPAddress = lib.mkForce mainIPv4.address; 56 | 57 | # Force-override the IP addresses of the interface so that we don't get the 58 | # adresses assigned by the NixOS testing system. 59 | networking.interfaces.${config.headcounter.mainDevice} = { 60 | ip4 = lib.mkForce (lib.singleton mainIPv4 ++ vhostCfg.ip4); 61 | ip6 = lib.mkForce (lib.singleton mainIPv6 ++ vhostCfg.ip6); 62 | }; 63 | 64 | networking.defaultGateway = { 65 | address = "0.0.0.0"; 66 | interface = "eth1"; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /modules/testing/resolver.nix: -------------------------------------------------------------------------------- 1 | # This module automatically discovers zones from BIND and NSD NixOS 2 | # configurations within the current test network and delegates these 3 | # zones from a fake root zone. 4 | { config, nodes, pkgs, lib, ... }: 5 | 6 | { 7 | services.bind.enable = true; 8 | services.bind.cacheNetworks = lib.mkForce [ "any" ]; 9 | services.bind.forwarders = lib.mkForce []; 10 | services.bind.zones = lib.singleton { 11 | name = "."; 12 | file = let 13 | addDot = zone: zone + lib.optionalString (!lib.hasSuffix "." zone) "."; 14 | mkNsdZoneNames = zones: map addDot (lib.attrNames zones); 15 | mkBindZoneNames = zones: map (zone: addDot zone.name) zones; 16 | getZones = cfg: mkNsdZoneNames cfg.services.nsd.zones 17 | ++ mkBindZoneNames cfg.services.bind.zones; 18 | 19 | getZonesForNode = attrs: { 20 | ip = attrs.config.networking.primaryIPAddress; 21 | zones = lib.filter (zone: zone != ".") (getZones attrs.config); 22 | }; 23 | 24 | zoneInfo = lib.mapAttrsToList (lib.const getZonesForNode) nodes; 25 | 26 | # All of the zones that are subdomains of existing zones. 27 | # For example if there is only "example.com" the following zones would be 28 | # 'subZones': 29 | # 30 | # * foo.example.com. 31 | # * bar.example.com. 32 | # 33 | # While the following would *not* be 'subZones': 34 | # 35 | # * example.com. 36 | # * com. 37 | # 38 | subZones = let 39 | allZones = lib.concatMap (zi: zi.zones) zoneInfo; 40 | isSubZoneOf = z1: z2: lib.hasSuffix z2 z1 && z1 != z2; 41 | in lib.filter (z: lib.any (isSubZoneOf z) allZones) allZones; 42 | 43 | # All the zones without 'subZones'. 44 | filteredZoneInfo = map (zi: zi // { 45 | zones = lib.filter (x: !lib.elem x subZones) zi.zones; 46 | }) zoneInfo; 47 | 48 | in pkgs.writeText "fake-root.zone" '' 49 | $TTL 3600 50 | . IN SOA ns.fakedns. admin.fakedns. ( 1 3h 1h 1w 1d ) 51 | ns.fakedns. IN A ${config.networking.primaryIPAddress} 52 | . IN NS ns.fakedns. 53 | ${lib.concatImapStrings (num: { ip, zones }: '' 54 | ns${toString num}.fakedns. IN A ${ip} 55 | ${lib.concatMapStrings (zone: '' 56 | ${zone} IN NS ns${toString num}.fakedns. 57 | '') zones} 58 | '') (lib.filter (zi: zi.zones != []) filteredZoneInfo)} 59 | ''; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /modules/testing/tshark.nix: -------------------------------------------------------------------------------- 1 | # Pluggable module for VM tests which attaches a sniffer on all network 2 | # interfaces (including the loopback interface). 3 | # 4 | # NOTE: Do *NOT* use this in production *ever*, because the sniffers are 5 | # running as root user and increase the attack surface substantially. 6 | { pkgs, ... }: 7 | 8 | { 9 | services.udev.extraRules = '' 10 | ACTION=="add", SUBSYSTEM=="net", KERNEL!="lo", TAG+="systemd", \ 11 | ENV{SYSTEMD_WANTS}="tshark@%k.service" 12 | ''; 13 | 14 | systemd.services."check-if-up@" = { 15 | description = "Wait For Network Interface %I To Become Available"; 16 | serviceConfig.ExecStart = "${pkgs.writeScript "check-if-up" '' 17 | #!${pkgs.stdenv.shell} 18 | while ${pkgs.coreutils}/bin/sleep 0.1; do 19 | [ "$(< "/sys/class/net/$1/carrier")" = 1 ] && break 20 | done 21 | ''} %I"; 22 | serviceConfig.Type = "oneshot"; 23 | }; 24 | 25 | systemd.services.tshark-lo = { 26 | description = "Attach Sniffer To Loopback Device"; 27 | wantedBy = [ "multi-user.target" ]; 28 | requires = [ "check-if-up@lo.service" ]; 29 | after = [ "check-if-up@lo.service" ]; 30 | serviceConfig = { 31 | SyslogIdentifier = "tshark-loopback"; 32 | ExecStart = "${pkgs.wireshark}/bin/tshark -n -i lo"; 33 | }; 34 | }; 35 | 36 | systemd.services."tshark@" = { 37 | description = "Attach Sniffer To Network Device %I"; 38 | bindsTo = [ "sys-subsystem-net-devices-%i.device" ]; 39 | requires = [ "check-if-up@%i.service" ]; 40 | after = [ 41 | "sys-subsystem-net-devices-%i.device" 42 | "check-if-up@%i.service" 43 | ]; 44 | serviceConfig = { 45 | SyslogIdentifier = "tshark-%I"; 46 | ExecStart = "${pkgs.wireshark}/bin/tshark -n -i %I"; 47 | }; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /network.nix: -------------------------------------------------------------------------------- 1 | let 2 | mkMachine = attrs: { 3 | imports = [ ./common-machines.nix ] 4 | ++ attrs.imports or []; 5 | } // removeAttrs attrs [ "imports" ]; 6 | in { 7 | network.description = "Headcounter Services"; 8 | network.enableRollback = true; 9 | 10 | resources.sshKeyPairs."hydra-build" = {}; 11 | 12 | ultron = { pkgs, lib, config, ... }: mkMachine { 13 | imports = [ ./machines/ultron ]; 14 | headcounter.mainIPv4 = "5.9.105.142"; 15 | headcounter.mainIPv6 = "2a01:4f8:162:4187::"; 16 | 17 | deployment.encryptedLinksTo = [ "dugee" "gussh" ]; 18 | 19 | deployment.hetzner.partitions = '' 20 | clearpart --all --initlabel --drives=sda,sdb 21 | 22 | part swap1 --recommended --label=swap1 --fstype=swap --ondisk=sda 23 | part swap2 --recommended --label=swap2 --fstype=swap --ondisk=sdb 24 | 25 | part btrfs.1 --grow --ondisk=sda 26 | part btrfs.2 --grow --ondisk=sdb 27 | 28 | btrfs / --data=1 --metadata=1 --label=root btrfs.1 btrfs.2 29 | ''; 30 | 31 | services.openssh.extraConfig = lib.mkAfter '' 32 | ListenAddress ${config.deployment.hetzner.mainIPv4} 33 | ListenAddress [2a01:4f8:162:4187::] 34 | ''; 35 | }; 36 | 37 | taalo = { pkgs, lib, nodes, config, ... }: let 38 | inherit (config.networking.p2pTunnels.ssh) ultron; 39 | in mkMachine { 40 | imports = [ ./hydra.nix ]; 41 | headcounter.mainIPv4 = "188.40.96.202"; 42 | headcounter.mainIPv6 = "2a01:4f8:221:17c6::"; 43 | 44 | fileSystems."/".options = [ 45 | "autodefrag" "space_cache" "compress=lzo" "noatime" 46 | ]; 47 | 48 | boot.kernelPackages = pkgs.linuxPackages_latest; 49 | 50 | deployment.hetzner.partitions = '' 51 | clearpart --all --initlabel --drives=sda,sdb 52 | 53 | part swap1 --size=10000 --label=swap1 --fstype=swap --ondisk=sda 54 | part swap2 --size=10000 --label=swap2 --fstype=swap --ondisk=sdb 55 | 56 | part btrfs.1 --grow --ondisk=sda 57 | part btrfs.2 --grow --ondisk=sdb 58 | 59 | btrfs / --data=1 --metadata=1 --label=root btrfs.1 btrfs.2 60 | ''; 61 | deployment.encryptedLinksTo = [ "ultron" ]; 62 | 63 | services.hydra-dev = { 64 | listenHost = lib.mkForce ultron.localIPv4; 65 | dbi = "dbi:Pg:dbname=hydra;user=hydra;host=${ultron.remoteIPv4}"; 66 | }; 67 | 68 | headcounter.conditions.hydra-init.custom.command = '' 69 | ${pkgs.postgresql}/bin/psql -h ${ultron.remoteIPv4} hydra hydra -c "" 70 | ''; 71 | }; 72 | 73 | benteflork = mkMachine { 74 | imports = [ ./hydra-slave.nix ]; 75 | headcounter.mainIPv4 = "144.76.202.147"; 76 | headcounter.mainIPv6 = "2a01:4f8:200:8392::"; 77 | }; 78 | 79 | dugee = { nodes, config, lib, ... }: mkMachine { 80 | imports = [ ./dns-server.nix ]; 81 | headcounter.services.acme.dnsHandler = let 82 | myself = config.networking.hostName; 83 | tunnel = nodes.ultron.config.networking.p2pTunnels.ssh.${myself}; 84 | in { 85 | enable = true; 86 | fqdn = "ns1.headcounter.org"; 87 | listen = lib.singleton { 88 | host = tunnel.remoteIPv4; 89 | device = "tun${toString tunnel.remoteTunnel}"; 90 | }; 91 | }; 92 | headcounter.mainIPv4 = "78.46.182.124"; 93 | headcounter.mainIPv6 = "2a01:4f8:d13:3009::2"; 94 | networking.localCommands = lib.mkAfter '' 95 | ip -6 addr add 2a01:4f8:d13:3009::2 dev ${config.headcounter.mainDevice} 96 | ''; 97 | }; 98 | 99 | gussh = { config, lib, ... }: mkMachine { 100 | imports = [ ./dns-server.nix ]; 101 | headcounter.mainIPv4 = "78.47.142.38"; 102 | headcounter.mainIPv6 = "2a01:4f8:d13:5308::2"; 103 | networking.localCommands = lib.mkAfter '' 104 | ip -6 addr add 2a01:4f8:d13:5308::2 dev ${config.headcounter.mainDevice} 105 | ''; 106 | }; 107 | 108 | unzervalt = { nodes, lib, ... }: mkMachine { 109 | deployment.targetEnv = "container"; 110 | deployment.container.host = nodes.ultron.config; 111 | imports = [ ./common.nix ] 112 | ++ lib.optional (lib.pathExists ./private/default.nix) ./private; 113 | headcounter.services.webspace.enable = true; 114 | users.mutableUsers = false; 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /pkgs/acmetool/absolute-symlinks.patch: -------------------------------------------------------------------------------- 1 | diff --git a/fdb/fdb.go b/fdb/fdb.go 2 | index f617fdc..5c1f618 100644 3 | --- a/fdb/fdb.go 4 | +++ b/fdb/fdb.go 5 | @@ -263,12 +263,7 @@ func (db *DB) conformPermissions() error { 6 | return err 7 | } 8 | 9 | - if filepath.IsAbs(l) { 10 | - return fmt.Errorf("database symlinks must not have absolute targets: %v: %v", path, l) 11 | - } 12 | - 13 | - ll := filepath.Join(filepath.Dir(path), l) 14 | - ll, err = filepath.Abs(ll) 15 | + ll, err := filepath.Abs(l) 16 | if err != nil { 17 | return err 18 | } 19 | @@ -757,12 +752,8 @@ func (c *Collection) WriteLink(name string, target Link) error { 20 | 21 | from := filepath.Join(c.db.path, c.name, name) 22 | to := filepath.Join(c.db.path, target.Target) 23 | - toRel, err := filepath.Rel(filepath.Dir(from), to) 24 | - if err != nil { 25 | - return err 26 | - } 27 | 28 | - tmpName, err := tempSymlink(toRel, filepath.Join(c.db.path, "tmp")) 29 | + tmpName, err := tempSymlink(to, filepath.Join(c.db.path, "tmp")) 30 | if err != nil { 31 | return err 32 | } 33 | diff --git a/storage/storage-fdb.go b/storage/storage-fdb.go 34 | index f7784aa..6ecec08 100644 35 | --- a/storage/storage-fdb.go 36 | +++ b/storage/storage-fdb.go 37 | @@ -14,6 +14,7 @@ import ( 38 | "io" 39 | "io/ioutil" 40 | "os" 41 | + "path/filepath" 42 | "strings" 43 | "time" 44 | ) 45 | @@ -129,7 +130,7 @@ func (s *fdbStore) loadPreferred() error { 46 | return err 47 | } 48 | 49 | - certID := link.Target[6:] 50 | + certID := filepath.Base(link.Target) 51 | cert := s.CertificateByID(certID) 52 | if cert == nil { 53 | // This should never happen because fdb checks symlinks, though maybe if 54 | -------------------------------------------------------------------------------- /pkgs/acmetool/desired-in-store.patch: -------------------------------------------------------------------------------- 1 | diff --git a/fdb/fdb.go b/fdb/fdb.go 2 | index f617fdc..46027e1 100644 3 | --- a/fdb/fdb.go 4 | +++ b/fdb/fdb.go 5 | @@ -566,7 +566,12 @@ func (c *Collection) Openl(name string) (ReadStream, error) { 6 | } 7 | 8 | func (c *Collection) open(name string, allowSymlinks bool) (ReadStream, error) { 9 | - fi, err := os.Lstat(filepath.Join(c.db.path, c.name, name)) 10 | + dbpath := filepath.Join(c.db.path, c.name, name) 11 | + if filepath.IsAbs(c.name) { 12 | + dbpath = filepath.Join(c.name, name) 13 | + } 14 | + 15 | + fi, err := os.Lstat(dbpath) 16 | again: 17 | if err != nil { 18 | return nil, err 19 | @@ -580,7 +585,7 @@ again: 20 | return nil, ErrIsLink 21 | } 22 | 23 | - fi, err = os.Stat(filepath.Join(c.db.path, c.name, name)) 24 | + fi, err = os.Stat(dbpath) 25 | goto again 26 | 27 | case os.ModeDir: 28 | @@ -589,7 +594,7 @@ again: 29 | return nil, fmt.Errorf("unknown file type") 30 | } 31 | 32 | - f, err := os.Open(filepath.Join(c.db.path, c.name, name)) 33 | + f, err := os.Open(dbpath) 34 | if err != nil { 35 | return nil, err 36 | } 37 | @@ -777,7 +782,11 @@ func (c *Collection) WriteLink(name string, target Link) error { 38 | } 39 | 40 | func (c *Collection) ListAll() ([]string, error) { 41 | - ms, err := filepath.Glob(filepath.Join(c.db.path, c.name, "*")) 42 | + dbglob := filepath.Join(c.db.path, c.name, "*") 43 | + if filepath.IsAbs(c.name) { 44 | + dbglob = filepath.Join(c.name, "*") 45 | + } 46 | + ms, err := filepath.Glob(dbglob) 47 | if err != nil { 48 | return nil, err 49 | } 50 | diff --git a/storage/storage-fdb.go b/storage/storage-fdb.go 51 | index f7784aa..263e4f0 100644 52 | --- a/storage/storage-fdb.go 53 | +++ b/storage/storage-fdb.go 54 | @@ -191,7 +191,6 @@ func init() { 55 | var storePermissions = []fdb.Permission{ 56 | {Path: ".", DirMode: 0755, FileMode: 0644}, 57 | {Path: "accounts", DirMode: 0700, FileMode: 0600}, 58 | - {Path: "desired", DirMode: 0755, FileMode: 0644}, 59 | {Path: "live", DirMode: 0755, FileMode: 0644}, 60 | {Path: "certs", DirMode: 0755, FileMode: 0644}, 61 | {Path: "certs/*/haproxy", DirMode: 0700, FileMode: 0600}, // hack for HAProxy 62 | @@ -564,7 +563,13 @@ func (s *fdbStore) loadTargets() error { 63 | s.loadRSAKeySize() 64 | 65 | // targets 66 | - c := s.db.Collection("desired") 67 | + desiredDir, found := os.LookupEnv("ACME_DESIRED_DIR") 68 | + 69 | + if !found { 70 | + desiredDir = "desired" 71 | + } 72 | + 73 | + c := s.db.Collection(desiredDir) 74 | 75 | desiredKeys, err := c.List() 76 | if err != nil { 77 | -------------------------------------------------------------------------------- /pkgs/acmetool/remove-unneded-responders.patch: -------------------------------------------------------------------------------- 1 | diff --git a/cmd/acmetool/main.go b/cmd/acmetool/main.go 2 | index 59d4ec2..a32d715 100644 3 | --- a/cmd/acmetool/main.go 4 | +++ b/cmd/acmetool/main.go 5 | @@ -13,15 +13,12 @@ import ( 6 | "github.com/hlandau/acme/acmeapi/acmeutils" 7 | "github.com/hlandau/acme/hooks" 8 | "github.com/hlandau/acme/interaction" 9 | - "github.com/hlandau/acme/redirector" 10 | - "github.com/hlandau/acme/responder" 11 | "github.com/hlandau/acme/storage" 12 | "github.com/hlandau/acme/storageops" 13 | "github.com/hlandau/dexlogconfig" 14 | "github.com/hlandau/xlog" 15 | "gopkg.in/alecthomas/kingpin.v2" 16 | "gopkg.in/hlandau/easyconfig.v1/adaptflag" 17 | - "gopkg.in/hlandau/service.v2" 18 | "gopkg.in/square/go-jose.v1" 19 | "gopkg.in/yaml.v2" 20 | ) 21 | @@ -160,8 +157,6 @@ func main() { 22 | cmdUnwant() 23 | case "quickstart": 24 | cmdQuickstart() 25 | - case "redirector": 26 | - cmdRunRedirector() 27 | case "test-notify": 28 | cmdRunTestNotify() 29 | case "import-key": 30 | @@ -366,42 +361,6 @@ func cmdUnwant() { 31 | } 32 | } 33 | 34 | -func cmdRunRedirector() { 35 | - rpath := *redirectorPathFlag 36 | - if rpath == "" { 37 | - // redirector process is internet-facing and must never touch private keys 38 | - storage.Neuter() 39 | - rpath = determineWebroot() 40 | - } 41 | - 42 | - service.Main(&service.Info{ 43 | - Name: "acmetool", 44 | - Description: "acmetool HTTP redirector", 45 | - DefaultChroot: rpath, 46 | - NewFunc: func() (service.Runnable, error) { 47 | - return redirector.New(redirector.Config{ 48 | - Bind: ":80", 49 | - ChallengePath: rpath, 50 | - ChallengeGID: *redirectorGIDFlag, 51 | - ReadTimeout: *redirectorReadTimeout, 52 | - WriteTimeout: *redirectorWriteTimeout, 53 | - }) 54 | - }, 55 | - }) 56 | -} 57 | - 58 | -func determineWebroot() string { 59 | - s, err := storage.NewFDB(*stateFlag) 60 | - log.Fatale(err, "storage") 61 | - 62 | - webrootPaths := s.DefaultTarget().Request.Challenge.WebrootPaths 63 | - if len(webrootPaths) > 0 { 64 | - return webrootPaths[0] 65 | - } 66 | - 67 | - return responder.StandardWebrootPath 68 | -} 69 | - 70 | func cmdRunTestNotify() { 71 | ctx := &hooks.Context{ 72 | HooksDir: *hooksFlag, 73 | diff --git a/storageops/reconcile.go b/storageops/reconcile.go 74 | index aefff4e..6507f24 100644 75 | --- a/storageops/reconcile.go 76 | +++ b/storageops/reconcile.go 77 | @@ -2,7 +2,6 @@ 78 | package storageops 79 | 80 | import ( 81 | - "bytes" 82 | "crypto" 83 | "crypto/rand" 84 | "crypto/x509" 85 | @@ -11,7 +10,6 @@ import ( 86 | "fmt" 87 | "github.com/hlandau/acme/acmeapi" 88 | "github.com/hlandau/acme/acmeapi/acmeendpoints" 89 | - "github.com/hlandau/acme/acmeapi/acmeutils" 90 | "github.com/hlandau/acme/hooks" 91 | "github.com/hlandau/acme/responder" 92 | "github.com/hlandau/acme/solver" 93 | @@ -414,22 +412,6 @@ func (r *reconcile) determineNecessaryAuthorizations(names []string, a *storage. 94 | return neededs 95 | } 96 | 97 | -func generateHookPEM(info *responder.TLSSNIChallengeInfo) (string, error) { 98 | - b := bytes.Buffer{} 99 | - 100 | - err := acmeutils.SaveCertificates(&b, info.Certificate) 101 | - if err != nil { 102 | - return "", err 103 | - } 104 | - 105 | - err = acmeutils.SavePrivateKey(&b, info.Key) 106 | - if err != nil { 107 | - return "", err 108 | - } 109 | - 110 | - return b.String(), nil 111 | -} 112 | - 113 | func (r *reconcile) obtainAuthorization(name string, a *storage.Account, targetFilename string, trc *storage.TargetRequestChallenge) error { 114 | cl := r.getClientForAccount(a) 115 | 116 | @@ -447,17 +429,6 @@ func (r *reconcile) obtainAuthorization(name string, a *storage.Account, targetF 117 | 118 | startHookFunc := func(challengeInfo interface{}) error { 119 | switch v := challengeInfo.(type) { 120 | - case *responder.HTTPChallengeInfo: 121 | - _, err := hooks.ChallengeHTTPStart(ctx, name, targetFilename, v.Filename, v.Body) 122 | - return err 123 | - case *responder.TLSSNIChallengeInfo: 124 | - hookPEM, err := generateHookPEM(v) 125 | - if err != nil { 126 | - return err 127 | - } 128 | - 129 | - _, err = hooks.ChallengeTLSSNIStart(ctx, name, targetFilename, v.Hostname1, v.Hostname2, hookPEM) 130 | - return err 131 | case *responder.DNSChallengeInfo: 132 | installed, err := hooks.ChallengeDNSStart(ctx, name, targetFilename, v.Body) 133 | if err == nil && !installed { 134 | @@ -471,16 +442,6 @@ func (r *reconcile) obtainAuthorization(name string, a *storage.Account, targetF 135 | 136 | stopHookFunc := func(challengeInfo interface{}) error { 137 | switch v := challengeInfo.(type) { 138 | - case *responder.HTTPChallengeInfo: 139 | - return hooks.ChallengeHTTPStop(ctx, name, targetFilename, v.Filename, v.Body) 140 | - case *responder.TLSSNIChallengeInfo: 141 | - hookPEM, err := generateHookPEM(v) 142 | - if err != nil { 143 | - return err 144 | - } 145 | - 146 | - _, err = hooks.ChallengeTLSSNIStop(ctx, name, targetFilename, v.Hostname1, v.Hostname2, hookPEM) 147 | - return err 148 | case *responder.DNSChallengeInfo: 149 | uninstalled, err := hooks.ChallengeDNSStop(ctx, name, targetFilename, v.Body) 150 | if err == nil && !uninstalled { 151 | -------------------------------------------------------------------------------- /pkgs/build-support/build-erlang/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv, erlang, rebar, writeEscript }: 2 | 3 | { name, version 4 | , buildInputs ? [], erlangDeps ? [] 5 | , postPatch ? "" 6 | , passthru ? {} 7 | , ... }@attrs: 8 | 9 | with stdenv.lib; 10 | 11 | let 12 | patchedRebar = stdenv.lib.overrideDerivation rebar (rb: { 13 | patches = [ ./rebar-nix.patch ]; 14 | }); 15 | 16 | recursiveErlangDeps = let 17 | getDeps = drv: [drv] ++ (map getDeps drv.erlangDeps); 18 | inputList = flatten (map getDeps erlangDeps); 19 | in uniqList { inherit inputList; }; 20 | 21 | self = stdenv.mkDerivation ({ 22 | name = "${name}-${version}"; 23 | 24 | buildInputs = buildInputs ++ [ erlang patchedRebar ]; 25 | 26 | postPatch = '' 27 | rm -f rebar 28 | 29 | "${writeEscript "rewrite-rebar-config" [] ./rewrite-rebar-config.erl}" \ 30 | rebar.config rebar.config.script || : 31 | 32 | "${writeEscript "rewrite-appfiles" [] ./rewrite-appfiles.erl}" \ 33 | "$(basename "$out" | cut -d- -f1)" "${name}" "${version}" 34 | 35 | ${postPatch} 36 | ''; 37 | 38 | configurePhase = '' 39 | runHook preConfigure 40 | runHook postConfigure 41 | ''; 42 | 43 | NIX_ERLANG_DEPENDENCIES = let 44 | mkDepMapping = d: "${d.packageName}=${d.appDir}"; 45 | in concatMapStringsSep ":" mkDepMapping recursiveErlangDeps; 46 | 47 | buildPhase = '' 48 | runHook preBuild 49 | rebar compile 50 | runHook postBuild 51 | ''; 52 | 53 | installPhase = '' 54 | runHook preInstall 55 | for reldir in ebin priv include; do 56 | [ -e "$reldir" ] || continue 57 | mkdir -p "$out/lib/erlang/lib/${name}" 58 | cp -rt "$out/lib/erlang/lib/${name}" "$reldir" 59 | done 60 | runHook postInstall 61 | ''; 62 | 63 | passthru = passthru // rec { 64 | packageName = name; 65 | libraryDir = "${self}/lib/erlang/lib"; 66 | appDir = "${libraryDir}/${packageName}"; 67 | inherit erlangDeps recursiveErlangDeps; 68 | }; 69 | } // removeAttrs attrs [ "name" "postPatch" "buildInputs" "passthru" ]); 70 | 71 | in self 72 | -------------------------------------------------------------------------------- /pkgs/build-support/build-erlang/rebar-nix.patch: -------------------------------------------------------------------------------- 1 | diff --git a/ebin/rebar.app b/ebin/rebar.app 2 | index 6e3609b..218b832 100644 3 | --- a/ebin/rebar.app 4 | +++ b/ebin/rebar.app 5 | @@ -26,6 +26,7 @@ 6 | rebar_lfe_compiler, 7 | rebar_log, 8 | rebar_neotoma_compiler, 9 | + rebar_nix, 10 | rebar_otp_app, 11 | rebar_port_compiler, 12 | rebar_proto_compiler, 13 | diff --git a/src/rebar_deps.erl b/src/rebar_deps.erl 14 | index 952276c..a8044e8 100644 15 | --- a/src/rebar_deps.erl 16 | +++ b/src/rebar_deps.erl 17 | @@ -377,7 +377,10 @@ find_dep(Config, Dep) -> 18 | %% e.g. {git, "https://github.com/mochi/mochiweb.git", "HEAD"} 19 | %% Deps with a source must be found (or fetched) locally. 20 | %% Those without a source may be satisfied from lib dir (get_lib_dir). 21 | - find_dep(Config, Dep, Dep#dep.source). 22 | + case rebar_nix:lookup_dep(Dep#dep.app) of 23 | + {ok, Path} -> {Config, {avail, Path}}; 24 | + false -> find_dep(Config, Dep, Dep#dep.source) 25 | + end. 26 | 27 | find_dep(Config, Dep, undefined) -> 28 | %% 'source' is undefined. If Dep is not satisfied locally, 29 | diff --git a/src/rebar_erlc_compiler.erl b/src/rebar_erlc_compiler.erl 30 | index e4abd3d..4e242ce 100644 31 | --- a/src/rebar_erlc_compiler.erl 32 | +++ b/src/rebar_erlc_compiler.erl 33 | @@ -748,9 +748,12 @@ maybe_expand_include_lib_path(File) -> 34 | expand_include_lib_path(File) -> 35 | File1 = filename:basename(File), 36 | Split = filename:split(filename:dirname(File)), 37 | - Lib = hd(Split), 38 | + Lib = list_to_atom(hd(Split)), 39 | SubDir = filename:join(tl(Split)), 40 | - Dir = code:lib_dir(list_to_atom(Lib), list_to_atom(SubDir)), 41 | + Dir = case rebar_nix:lookup_dep(Lib) of 42 | + {ok, StorePath} -> filename:join(StorePath, SubDir); 43 | + false -> code:lib_dir(Lib, list_to_atom(SubDir)) 44 | + end, 45 | filename:join(Dir, File1). 46 | 47 | %% 48 | diff --git a/src/rebar_nix.erl b/src/rebar_nix.erl 49 | index e69de29..481dab7 100644 50 | --- a/src/rebar_nix.erl 51 | +++ b/src/rebar_nix.erl 52 | @@ -0,0 +1,26 @@ 53 | +-module(rebar_nix). 54 | + 55 | +-export([get_depmap/0, lookup_dep/1]). 56 | + 57 | +-spec get_depmap(string()) -> [{atom(), string()}]. 58 | +get_depmap(DepMap) -> 59 | + SplitFun = fun($=) -> false; (_) -> true end, 60 | + MapFun = fun(S) -> 61 | + {Dep, [_|Path]} = lists:splitwith(SplitFun, S), 62 | + {list_to_atom(Dep), Path} 63 | + end, 64 | + lists:map(MapFun, string:tokens(DepMap, ":")). 65 | + 66 | +-spec get_depmap() -> [{atom(), string()}]. 67 | +get_depmap() -> 68 | + case os:getenv("NIX_ERLANG_DEPENDENCIES") of 69 | + false -> []; 70 | + DepMap -> get_depmap(DepMap) 71 | + end. 72 | + 73 | +-spec lookup_dep(atom()) -> {ok, string()} | false. 74 | +lookup_dep(Dep) -> 75 | + case lists:keyfind(Dep, 1, get_depmap()) of 76 | + {_, Path} -> {ok, Path}; 77 | + false -> false 78 | + end. 79 | diff --git a/src/rebar_rel_utils.erl b/src/rebar_rel_utils.erl 80 | index 068fa1c..ad59926 100644 81 | --- a/src/rebar_rel_utils.erl 82 | +++ b/src/rebar_rel_utils.erl 83 | @@ -136,7 +136,8 @@ get_previous_release_path(Config) -> 84 | load_config(Config, ReltoolFile) -> 85 | case rebar_config:consult_file(ReltoolFile) of 86 | {ok, Terms} -> 87 | - expand_version(Config, Terms, filename:dirname(ReltoolFile)); 88 | + Terms1 = expand_libs(Terms), 89 | + expand_version(Config, Terms1, filename:dirname(ReltoolFile)); 90 | Other -> 91 | ?ABORT("Failed to load expected config from ~s: ~p~n", 92 | [ReltoolFile, Other]) 93 | @@ -234,6 +235,19 @@ make_proplist([H|T], Acc) -> 94 | make_proplist([], Acc) -> 95 | Acc. 96 | 97 | +expand_libs(ReltoolConfig) -> 98 | + FullDeps = lists:map(fun({_, Path}) -> filename:dirname(Path) end, rebar_nix:get_depmap()), 99 | + NewSys = case lists:keyfind(sys, 1, ReltoolConfig) of 100 | + {sys, Sys} -> 101 | + NewDeps = case lists:keyfind(lib_dirs, 1, Sys) of 102 | + {lib_dirs, L} -> {lib_dirs, L ++ FullDeps}; 103 | + false -> {lib_dirs, FullDeps} 104 | + end, 105 | + {sys, lists:keyreplace(lib_dirs, 1, Sys, NewDeps)}; 106 | + false -> {sys, [{lib_dirs, FullDeps}]} 107 | + end, 108 | + lists:keyreplace(sys, 1, ReltoolConfig, NewSys). 109 | + 110 | expand_version(Config, ReltoolConfig, Dir) -> 111 | case lists:keyfind(sys, 1, ReltoolConfig) of 112 | {sys, Sys} -> 113 | -------------------------------------------------------------------------------- /pkgs/build-support/build-erlang/rewrite-appfiles.erl: -------------------------------------------------------------------------------- 1 | -module('rewrite-appfiles'). 2 | -export([main/1]). 3 | 4 | -spec get_app_trylist(atom()) -> [file:filename_all()]. 5 | get_app_trylist(AppName) -> 6 | NameStr = atom_to_list(AppName), 7 | [ filename:join("src", NameStr ++ ".app.src") 8 | , filename:join("src", NameStr ++ ".app") 9 | , filename:join("ebin", NameStr ++ ".app") 10 | ]. 11 | 12 | -spec get_app_file(file:name(), atom()) -> {ok, file:name()} | not_found. 13 | get_app_file(Basedir, AppName) -> 14 | ToTry = [filename:join(Basedir, Try) || Try <- get_app_trylist(AppName)], 15 | case lists:filter(fun filelib:is_file/1, ToTry) of 16 | [First|_] -> {ok, First}; 17 | _ -> not_found 18 | end. 19 | 20 | -spec get_app_file(file:name()) -> file:name(). 21 | get_app_file(Basedir) -> 22 | AppName = case filename:basename(Basedir) of 23 | Base when is_list(Base) -> list_to_atom(Base); 24 | Base when is_binary(Base) -> binary_to_atom(Base, unicode) 25 | end, 26 | get_app_file(Basedir, AppName). 27 | 28 | -spec get_appfiles(string()) -> {[file:filename_all()], [file:filename_all()]}. 29 | get_appfiles(Name) -> 30 | SubAppFiles = case file:list_dir("apps") of 31 | {error, _} -> []; 32 | {ok, Files} -> 33 | Dirs = [F || F <- Files, filelib:is_dir(filename:join("apps", F))], 34 | AppFiles = [get_app_file(filename:join("apps", D)) || D <- Dirs], 35 | Filter = fun({ok, File}) -> {true, File}; 36 | (_) -> false 37 | end, 38 | lists:filtermap(Filter, AppFiles) 39 | end, 40 | 41 | MainAppFiles = case get_app_file(".", list_to_atom(Name)) of 42 | {ok, AppFile} -> [AppFile]; 43 | _ -> [] 44 | end, 45 | 46 | {MainAppFiles, SubAppFiles}. 47 | 48 | -spec rewrite_version(Vsn :: string(), Hash :: string()) -> string(). 49 | rewrite_version(Vsn, Hash) when is_list(Vsn) -> Hash ++ [$_|Vsn]; 50 | rewrite_version(_, Hash) -> Hash. 51 | 52 | -spec rewrite_appver( file:filename_all() 53 | , string() 54 | , string() | keep_orig 55 | ) -> ok | {error, atom()}. 56 | rewrite_appver(File, Hash, Ver) -> 57 | {ok, [{application, AppName, Keys}]} = file:consult(File), 58 | {vsn, OrigVsn} = lists:keyfind(vsn, 1, Keys), 59 | NewVsn = case Ver of 60 | keep_orig -> rewrite_version(OrigVsn, Hash); 61 | Replacement -> rewrite_version(Replacement, Hash) 62 | end, 63 | NewKeys = lists:keyreplace(vsn, 1, Keys, {vsn, NewVsn}), 64 | NewData = {application, AppName, NewKeys}, 65 | Result = file:write_file(File, [io_lib:format("~tp.~n", [NewData])]), 66 | Msg = "Rewrote ~s from version ~tp to ~tp.~n", 67 | io:fwrite(standard_error, Msg, [File, OrigVsn, NewVsn]), 68 | Result. 69 | 70 | -spec do_strip_comments(binary(), binary()) -> binary(). 71 | do_strip_comments(<<$%, Rest/binary>>, Acc) -> 72 | case binary:split(Rest, <<$\n>>) of 73 | [_, NextLines] -> do_strip_comments(NextLines, Acc); 74 | _ -> do_strip_comments(Rest, <>) 75 | end; 76 | do_strip_comments(<>, Acc) -> 77 | do_strip_comments(Rest, <>); 78 | do_strip_comments(<<>>, Acc) -> 79 | Acc. 80 | 81 | -spec strip_comments(file:filename_all()) -> file:filename_all(). 82 | strip_comments(File) -> 83 | case file:read_file(File) of 84 | {ok, Data} -> 85 | Stripped = do_strip_comments(Data, <<>>), 86 | file:write_file(File, Stripped), 87 | File; 88 | _ -> 89 | File 90 | end. 91 | 92 | -spec strip_files([file:filename_all()]) -> [file:filename_all()]. 93 | strip_files(Files) -> lists:map(fun strip_comments/1, Files). 94 | 95 | -spec err_out(string()) -> no_return(). 96 | err_out(Msg) -> 97 | io:fwrite(standard_error, "~s~n", [Msg]), 98 | halt(1). 99 | 100 | main([Hash, Name, MainVersion]) -> 101 | {MainAppFiles, SubAppFiles} = case get_appfiles(Name) of 102 | {ToStrip1, ToStrip2} -> {strip_files(ToStrip1), strip_files(ToStrip2)} 103 | end, 104 | 105 | MainRewritten = [rewrite_appver(F, Hash, MainVersion) || F <- MainAppFiles], 106 | SubRewritten = [rewrite_appver(F, Hash, keep_orig) || F <- SubAppFiles], 107 | 108 | Rewritten = case MainRewritten ++ SubRewritten of 109 | [] -> err_out("Cannot find .app files to rewrite!"); 110 | AF -> AF 111 | end, 112 | 113 | case lists:all(fun(ok) -> true; (_) -> false end, Rewritten) of 114 | false -> err_out("Error rewriting application files!"); 115 | true -> halt(0) 116 | end. 117 | -------------------------------------------------------------------------------- /pkgs/build-support/build-erlang/rewrite-rebar-config.erl: -------------------------------------------------------------------------------- 1 | -module('rewrite-rebar-config'). 2 | -export([main/1]). 3 | 4 | do_rewrite(File) -> 5 | try 6 | {ok, Original} = file:consult(File), 7 | {deps, OrigDeps} = lists:keyfind(deps, 1, Original), 8 | NewDeps = [{Name, ".*", Src} || {Name, _, Src} <- OrigDeps], 9 | Rewritten = lists:keyreplace(deps, 1, Original, {deps, NewDeps}), 10 | PPrint = fun(T) -> 11 | io_lib:format("~tp.~n", [T]) 12 | end, 13 | Data = lists:map(PPrint, Rewritten), 14 | file:write_file(File, Data) 15 | catch 16 | _:_ -> {error, ignored} 17 | end. 18 | 19 | main(Files) -> 20 | lists:map(fun do_rewrite/1, Files). 21 | -------------------------------------------------------------------------------- /pkgs/build-support/compile-c.nix: -------------------------------------------------------------------------------- 1 | { lib, runCommandCC }: 2 | 3 | { name, source, cflags ? [] }: 4 | 5 | runCommandCC name { inherit source; } '' 6 | echo "$source" | gcc -Wall \ 7 | ${lib.concatMapStringsSep " " lib.escapeShellArg cflags} \ 8 | -xc - -o "$out" 9 | '' 10 | -------------------------------------------------------------------------------- /pkgs/build-support/compile-haskell.nix: -------------------------------------------------------------------------------- 1 | { lib, runCommand, haskellPackages, writeText }: 2 | 3 | { name, source, ghcflags ? [], buildDepends ? [] }: 4 | 5 | let 6 | mkDep = hpkgs: dep: if lib.isString dep then hpkgs.${dep} else dep; 7 | in runCommand name { 8 | src = if lib.isString source then writeText "${name}.hs" source else source; 9 | ghc = haskellPackages.ghcWithPackages (p: map (mkDep p) buildDepends); 10 | } '' 11 | "$ghc/bin/ghc" ${lib.concatMapStringsSep " " lib.escapeShellArg ghcflags} \ 12 | --make "$src" -odir . -o "$out" 13 | '' 14 | -------------------------------------------------------------------------------- /pkgs/build-support/write-escript.nix: -------------------------------------------------------------------------------- 1 | { stdenv, erlang, writeText }: 2 | 3 | name: emuArgs: textOrFile: 4 | 5 | with stdenv.lib; 6 | with import ../../lib { inherit (stdenv) lib; }; 7 | 8 | let 9 | mkFile = writeText "${name}.erl"; 10 | scriptFile = if isString textOrFile then mkFile textOrFile else textOrFile; 11 | 12 | erlCommands = writeText "write-escript.erl" '' 13 | % skipped line, escript is executed directly 14 | main([Name]) -> 15 | CompileOpts = [binary, {warn_format, 2}, report], 16 | {ok, _, BeamCode} = compile:file(Name, CompileOpts), 17 | EmuArgs = [${concatStringsSep ", " (map erlString emuArgs)}], 18 | MaybeEmuArgs = case length(EmuArgs) of 19 | 0 -> []; 20 | _ -> [{emu_args, EmuArgs}] 21 | end, 22 | Out = case os:getenv("out") of 23 | false -> halt("Environment variable $out is not set!"); 24 | Path -> Path 25 | end, 26 | ok = escript:create(Out, [ 27 | {shebang, "${erlang}/bin/escript"}, 28 | {beam, BeamCode} 29 | ] ++ MaybeEmuArgs), 30 | halt(0). 31 | ''; 32 | 33 | in stdenv.mkDerivation { 34 | inherit name; 35 | buildCommand = '' 36 | cat "${scriptFile}" > "$name.erl" 37 | "${erlang}/bin/escript" "${erlCommands}" "$name.erl" 38 | chmod +x "$out" 39 | ''; 40 | } 41 | -------------------------------------------------------------------------------- /pkgs/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | let 4 | callPackage = pkgs.lib.callPackageWith (pkgs // headcounter); 5 | 6 | headcounter = rec { 7 | compileC = callPackage ./build-support/compile-c.nix {}; 8 | compileHaskell = callPackage ./build-support/compile-haskell.nix {}; 9 | buildErlang = callPackage ./build-support/build-erlang {}; 10 | writeEscript = callPackage ./build-support/write-escript.nix {}; 11 | 12 | nexus = pkgs.haskellPackages.callPackage ./nexus {}; 13 | 14 | mongooseim = callPackage ./mongooseim {}; 15 | mongooseimTests = callPackage ./mongooseim/tests {}; 16 | spectrum2 = callPackage ./spectrum2 {}; 17 | 18 | acmetool = callPackage ./acmetool {}; 19 | 20 | site = callPackage ./site {}; 21 | 22 | erlangPackages = callPackage ./erlang-packages.nix { 23 | inherit pkgs buildErlang writeEscript; 24 | }; 25 | 26 | # dependencies for spectrum2 27 | libcommuni = callPackage ./spectrum2/libcommuni.nix {}; 28 | libpqxx = callPackage ./spectrum2/libpqxx.nix {}; 29 | swiften = callPackage ./spectrum2/swiften.nix {}; 30 | 31 | xmppoke = callPackage ./xmppoke { 32 | lua = pkgs.lua5_1; 33 | luaPackages = pkgs.lua51Packages; 34 | }; 35 | xmppokeReport = callPackage ./xmppoke/genreport.nix {}; 36 | }; 37 | in pkgs // { 38 | inherit headcounter; 39 | } 40 | -------------------------------------------------------------------------------- /pkgs/mongooseim/allow-set-default-mam-archive-mode.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/mod_mam.erl b/apps/ejabberd/src/mod_mam.erl 2 | index c56a7704..34eeaead 100644 3 | --- a/apps/ejabberd/src/mod_mam.erl 4 | +++ b/apps/ejabberd/src/mod_mam.erl 5 | @@ -448,7 +448,9 @@ handle_set_prefs_result({error, Reason}, 6 | handle_get_prefs(ArcJID=#jid{}, IQ=#iq{}) -> 7 | Host = server_host(ArcJID), 8 | ArcID = archive_id_int(Host, ArcJID), 9 | - Res = get_prefs(Host, ArcID, ArcJID, always), 10 | + DefOpt = gen_mod:get_module_opt(Host, mod_mam_meta, pm_archive_mode, 11 | + always), 12 | + Res = get_prefs(Host, ArcID, ArcJID, DefOpt), 13 | handle_get_prefs_result(Res, IQ). 14 | 15 | handle_get_prefs_result({DefaultMode, AlwaysJIDs, NeverJIDs}, IQ) -> 16 | @@ -677,7 +679,9 @@ is_interesting(LocJID, RemJID) -> 17 | is_interesting(Host, LocJID, RemJID, ArcID). 18 | 19 | is_interesting(Host, LocJID, RemJID, ArcID) -> 20 | - case get_behaviour(Host, ArcID, LocJID, RemJID, always) of 21 | + DefOpt = gen_mod:get_module_opt(Host, mod_mam_meta, pm_archive_mode, 22 | + always), 23 | + case get_behaviour(Host, ArcID, LocJID, RemJID, DefOpt) of 24 | always -> true; 25 | never -> false; 26 | roster -> is_jid_in_user_roster(LocJID, RemJID) 27 | @@ -698,7 +702,7 @@ archive_size(Host, ArcID, ArcJID=#jid{}) -> 28 | 29 | 30 | -spec get_behaviour(ejabberd:server(), archive_id(), LocJID :: ejabberd:jid(), 31 | - RemJID :: ejabberd:jid(), Default :: 'always') -> atom(). 32 | + RemJID :: ejabberd:jid(), Default :: archive_behaviour()) -> atom(). 33 | get_behaviour(Host, ArcID, 34 | LocJID=#jid{}, 35 | RemJID=#jid{}, DefaultBehaviour) -> 36 | -------------------------------------------------------------------------------- /pkgs/mongooseim/ctl-set-config.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/ejabberd_admin.erl b/apps/ejabberd/src/ejabberd_admin.erl 2 | index 41e9105b..ce208e3c 100644 3 | --- a/apps/ejabberd/src/ejabberd_admin.erl 4 | +++ b/apps/ejabberd/src/ejabberd_admin.erl 5 | @@ -31,6 +31,7 @@ 6 | %% Server 7 | status/0, 8 | send_service_message_all_mucs/2, 9 | + set_config/1, 10 | %% Accounts 11 | register/3, unregister/2, 12 | registered_users/1, 13 | @@ -142,6 +143,10 @@ commands() -> 14 | desc = "Reload configuration file on the current node", 15 | module = ejabberd_config, function = reload_local, 16 | args = [], result = {res, restuple}}, 17 | + #ejabberd_commands{name = set_config, tags = [server], 18 | + desc = "Set the configuration file to path.", 19 | + module = ?MODULE, function = set_config, 20 | + args = [{path, string}], result = {res, rescode}}, 21 | #ejabberd_commands{name = reload_cluster, tags = [server], 22 | desc = "Reload configuration file in the cluster", 23 | module = ejabberd_config, function = reload_cluster, 24 | @@ -289,6 +294,10 @@ send_service_message_all_mucs(Subject, AnnouncementText) -> 25 | end, 26 | ?MYHOSTS). 27 | 28 | +-spec set_config(ConfigFile :: file:name()) -> ok. 29 | +set_config(ConfigFile) -> 30 | + application:set_env(ejabberd, config, ConfigFile). 31 | + 32 | %%% 33 | %%% Account management 34 | %%% 35 | -------------------------------------------------------------------------------- /pkgs/mongooseim/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv, buildErlang, fetchFromGitHub, pam, zlib, expat, writeText 2 | , erlangPackages 3 | 4 | , storageBackends ? [ "pgsql" ] 5 | }: 6 | 7 | let 8 | inherit (stdenv.lib) concatLists concatMap concatMapStringsSep; 9 | 10 | backendDeps = { 11 | pgsql = [ "p1_pgsql" ]; 12 | mysql = [ "p1_mysql" ]; 13 | riak = [ "riakc" ]; 14 | cassandra = [ "cqerl" ]; 15 | }; 16 | 17 | getErlDeps = backend: backendDeps.${backend} or []; 18 | enabledBackendDeps = concatMap getErlDeps storageBackends; 19 | backendErlangDeps = map (dep: erlangPackages.${dep}) enabledBackendDeps; 20 | 21 | internalVersion = "2.1.8+mim-${self.version}"; 22 | 23 | relativeMainAppDir = let 24 | hash = builtins.head (builtins.match "([^-]*).*" (baseNameOf self)); 25 | hashVer = "${hash}_${internalVersion}"; 26 | in "lib/ejabberd-${hashVer}"; 27 | 28 | self = buildErlang rec { 29 | name = "mongooseim"; 30 | version = "2.0.1"; 31 | 32 | src = fetchFromGitHub { 33 | owner = "esl"; 34 | repo = "MongooseIM"; 35 | rev = version; 36 | sha256 = "18j2chhvq1pm78ay3gb19yjqifdyzxgbjz8hycjp529sxji3fdzq"; 37 | }; 38 | 39 | patches = [ 40 | ./reltool.patch ./journald.patch ./systemd.patch ./ctl-set-config.patch 41 | ./s2s-listener-certfile.patch ./pgsql-disable-escape-string-warning.patch 42 | ./tls-fixes.patch ./allow-set-default-mam-archive-mode.patch 43 | ./pep-mod-caps-dep.patch ./domain-certfile-binary.patch 44 | ]; 45 | 46 | prePatch = '' 47 | sed -i -e 's/{vsn, {cmd, [^}]*}}/{vsn, "2.1.8+mim-${version}"}/' \ 48 | apps/ejabberd/src/ejabberd.app.src 49 | ''; 50 | 51 | postPatch = let 52 | inherit (stdenv.lib) subtractLists filterAttrs attrValues escapeShellArg; 53 | allBackends = [ "pgsql" "mysql" "riak" "cassandra" ]; 54 | removedBackends = subtractLists storageBackends allBackends; 55 | mkFindRemoveArgs = concatMapStringsSep " -o " (rem: "-iname '*${rem}*'"); 56 | filterBackend = backend: stdenv.lib.elem backend removedBackends; 57 | maybeRemoveSed = backend: let 58 | sedArg = "-e '/mongoose_${backend}:start/d'"; 59 | in stdenv.lib.optionalString (filterBackend backend) sedArg; 60 | removedBackendDepAttrs = filterAttrs (n: v: filterBackend n) backendDeps; 61 | removedBackendDeps = concatLists (attrValues removedBackendDepAttrs); 62 | removedRebarDeps = [ "lager_syslog" ] ++ removedBackendDeps; 63 | in '' 64 | # Remove source files that have backends that should be removed in its 65 | # name. 66 | find apps \( ${mkFindRemoveArgs removedBackends} \) -type f -delete 67 | 68 | # Remove a bunch of rebar dependencies we don't need. 69 | escript "${writeText "remove-unneded-rebar-deps.escript" '' 70 | # Dummy comment, skipped by escript! 71 | main(Args) -> 72 | {ok, Orig} = file:consult("rebar.config"), 73 | {deps, OrigDeps} = lists:keyfind(deps, 1, Orig), 74 | DepsToRemove = lists:map(fun erlang:list_to_atom/1, Args), 75 | NewDeps = [Dep || {Name, _, _} = Dep <- OrigDeps, 76 | not lists:member(Name, DepsToRemove)], 77 | ReplacedDeps = lists:keyreplace(deps, 1, Orig, {deps, NewDeps}), 78 | New = case lists:member(riakc, DepsToRemove) of 79 | true -> lists:keydelete(pre_hooks, 1, ReplacedDeps); 80 | false -> ReplacedDeps 81 | end, 82 | Data = lists:map(fun(T) -> io_lib:format("~tp.~n", [T]) end, New), 83 | file:write_file("rebar.config", Data). 84 | ''}" ${concatMapStringsSep " " escapeShellArg removedRebarDeps} 85 | 86 | # Remove occurences of mongoose_$backend:start() during app startup. 87 | sed -i ${concatMapStringsSep " " maybeRemoveSed ["riak" "cassandra"]} \ 88 | apps/ejabberd/src/ejabberd_app.erl 89 | 90 | # Remove lager_syslog stuff, because we're doing logging via journald. 91 | sed -i -e '/^AppsToRun *= *\[/,/\]/{ /^ *\(lager_\)\?syslog *, *$/d }' \ 92 | rel/reltool.config.script 93 | 94 | patchShebangs tools/configure 95 | ''; 96 | 97 | postConfigure = let 98 | anyArgs = concatMapStringsSep " " (bend: "with-${bend}") storageBackends; 99 | backendArgs = if storageBackends != [] then anyArgs else "with-none"; 100 | in "./tools/configure ${backendArgs}"; 101 | 102 | buildInputs = [ pam zlib expat ]; 103 | erlangDeps = with erlangPackages; [ 104 | alarms base16 cache_tab cowboy cuesport ecoveralls exml exometer fast_tls 105 | folsom fusco idna jiffy lager lasse mochijson2 mustache pa proper poolboy 106 | recon redo sd_notify stringprep usec uuid 107 | ] ++ backendErlangDeps; 108 | 109 | postBuild = '' 110 | make rel/vars.config 111 | rebar generate 112 | ''; 113 | 114 | installPhase = '' 115 | cp -a "rel/${name}" "$out" 116 | 117 | intVer=${stdenv.lib.escapeShellArg internalVersion} 118 | hashVer="$(basename "$out" | cut -d- -f1)_$intVer" 119 | mainAppDir="$out/lib/ejabberd-$hashVer" 120 | if [ ! -d "$mainAppDir" ]; then 121 | echo "$mainAppDir does not exist!" >&2 122 | exit 1 123 | fi 124 | ''; 125 | 126 | passthru.mainAppDir = "${self}/${relativeMainAppDir}"; 127 | 128 | meta = { 129 | homepage = "https://www.erlang-solutions.com/products/" 130 | + "mongooseim-massively-scalable-ejabberd-platform"; 131 | description = "An Ejabberd fork utilizing Erlang/OTP features."; 132 | license = stdenv.lib.licenses.gpl2Plus; 133 | }; 134 | }; 135 | 136 | in self 137 | -------------------------------------------------------------------------------- /pkgs/mongooseim/domain-certfile-binary.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/ejabberd_s2s_out.erl b/apps/ejabberd/src/ejabberd_s2s_out.erl 2 | index 886fe088..04a7f5e0 100644 3 | --- a/apps/ejabberd/src/ejabberd_s2s_out.erl 4 | +++ b/apps/ejabberd/src/ejabberd_s2s_out.erl 5 | @@ -657,7 +657,7 @@ wait_for_starttls_proceed({xmlstreamelement, El}, StateData) -> 6 | Socket = StateData#state.socket, 7 | TLSOpts = case ejabberd_config:get_local_option( 8 | {domain_certfile, 9 | - binary_to_list(StateData#state.myname)}) of 10 | + StateData#state.myname}) of 11 | undefined -> 12 | StateData#state.tls_options; 13 | CertFile -> 14 | -------------------------------------------------------------------------------- /pkgs/mongooseim/journald.patch: -------------------------------------------------------------------------------- 1 | diff --git a/rel/files/app.config b/rel/files/app.config 2 | index 5a45eb6b..e71e148a 100644 3 | --- a/rel/files/app.config 4 | +++ b/rel/files/app.config 5 | @@ -1,14 +1,11 @@ 6 | [ 7 | - {{mongooseim_mdb_dir_toggle}}{mnesia, [{dir, "{{mongooseim_mdb_dir}}"}]}, 8 | {ssl, [{session_lifetime, 600}]}, %% 10 minutes 9 | {lager, [ 10 | - {log_root, "{{mongooseim_log_dir}}"}, 11 | - {crash_log, "crash.log"}, 12 | + {crash_log, undefined}, 13 | + {async_threshold, undefined}, 14 | + {error_logger_hwm, undefined}, 15 | {handlers, [ 16 | - {lager_console_backend, [info, {lager_default_formatter,[{eol, "\r\n"}]}]}, 17 | -%% use below line to add syslog backend for Lager 18 | -% {lager_syslog_backend, [ "mongooseim", local0, info]}, 19 | - {lager_file_backend, [{file, "ejabberd.log"}, {level, info}, {size, 2097152}, {date, "$D0"}, {count, 5}]} 20 | + {lager_console_backend, [info, {lager_default_formatter,[{eol, "\n"}]}]} 21 | ]} 22 | ]} 23 | %{exometer, [ 24 | -------------------------------------------------------------------------------- /pkgs/mongooseim/pep-mod-caps-dep.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/mod_pubsub.erl b/apps/ejabberd/src/mod_pubsub.erl 2 | index c139ee93..4239f17d 100644 3 | --- a/apps/ejabberd/src/mod_pubsub.erl 4 | +++ b/apps/ejabberd/src/mod_pubsub.erl 5 | @@ -90,7 +90,7 @@ 6 | %% API and gen_server callbacks 7 | -export([start_link/2, start/2, stop/1, init/1, 8 | handle_call/3, handle_cast/2, handle_info/2, 9 | - terminate/2, code_change/3]). 10 | + terminate/2, code_change/3, deps/2]). 11 | -export([default_host/0]). 12 | 13 | -export([send_loop/1]). 14 | @@ -4268,3 +4268,7 @@ timestamp() -> 15 | 16 | db_type(_Host) -> 17 | mnesia. 18 | + 19 | +deps(_Host, Opts) -> 20 | + [{mod_caps, hard} || P <- gen_mod:get_opt(plugins, Opts, []), 21 | + P == <<"pep">>]. 22 | -------------------------------------------------------------------------------- /pkgs/mongooseim/pgsql-disable-escape-string-warning.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/ejabberd_odbc.erl b/apps/ejabberd/src/ejabberd_odbc.erl 2 | index 7e3de74c..c6becf62 100644 3 | --- a/apps/ejabberd/src/ejabberd_odbc.erl 4 | +++ b/apps/ejabberd/src/ejabberd_odbc.erl 5 | @@ -660,6 +660,8 @@ pgsql_connect(Server, Port, DB, Username, Password) -> 6 | {ok, Ref} -> 7 | {ok, [<<"SET">>]} = 8 | pgsql:squery(Ref, "SET standard_conforming_strings=off;", ?QUERY_TIMEOUT), 9 | + {ok, [<<"SET">>]} = 10 | + pgsql:squery(Ref, "SET escape_string_warning=off;", ?QUERY_TIMEOUT), 11 | {ok, Ref}; 12 | Err -> Err 13 | end. 14 | -------------------------------------------------------------------------------- /pkgs/mongooseim/reltool.patch: -------------------------------------------------------------------------------- 1 | diff --git a/rel/reltool.config.script b/rel/reltool.config.script 2 | index 4066eceb..23ae5b08 100644 3 | --- a/rel/reltool.config.script 4 | +++ b/rel/reltool.config.script 5 | @@ -93,7 +93,7 @@ IncludeApps = lists:map(fun(App) -> {app, App, [{incl_cond, include}]} end, Apps 6 | 7 | 8 | [{sys, [ 9 | - {lib_dirs, ["../apps", "../deps"]}, 10 | + {lib_dirs, ["../apps"]}, 11 | {incl_cond, exclude}, 12 | {rel, "mongooseim", "", [mongooseim | AppsToRun]}, 13 | {rel, "start_clean", "", [kernel,stdlib]}, 14 | @@ -102,6 +102,8 @@ IncludeApps = lists:map(fun(App) -> {app, App, [{incl_cond, include}]} end, Apps 15 | {excl_archive_filters, [".*"]}, % do no archive built libs 16 | {excl_sys_filters, ["^bin/.*", 17 | "^erts.*/bin/(dialyzer|typer)"]}, 18 | + {excl_lib, otp_root}, 19 | + {relocatable, false}, 20 | 21 | {app, mongooseim, [{incl_cond, include}, {lib_dir, ".."}]} 22 | ] ++ IncludeApps}, 23 | @@ -111,24 +113,7 @@ IncludeApps = lists:map(fun(App) -> {app, App, [{incl_cond, include}]} end, Apps 24 | 25 | {overlay_vars, "vars.config"}, 26 | 27 | -{overlay, [ 28 | - {mkdir, "priv"}, 29 | - {copy, "files/sample_external_auth.py", "priv/sample_external_auth.py"}, 30 | - 31 | - {mkdir, "priv/ssl"}, 32 | - {copy, "../fake_cert.pem", "priv/ssl/fake_cert.pem"}, 33 | - {copy, "../fake_key.pem", "priv/ssl/fake_key.pem"}, 34 | - {copy, "../fake_server.pem", "priv/ssl/fake_server.pem"}, 35 | - {copy, "../fake_dh_server.pem", "priv/ssl/fake_dh_server.pem"}, 36 | - 37 | - {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, 38 | - {copy, "files/nodetool", "\{\{erts_vsn\}\}/bin/nodetool"}, 39 | - {copy, "files/mongooseimctl", "bin/mongooseimctl"}, 40 | - 41 | - {template, "files/mongooseim", "bin/mongooseim"}, 42 | - {template, "files/mongooseimctl", "bin/mongooseimctl"}, 43 | - {template, "files/app.config", "etc/app.config"}, 44 | - {template, "files/vm.args", "etc/vm.args"}, 45 | - {template, "files/ejabberd.cfg", "etc/ejabberd.cfg"} 46 | +{overlay, [{template, "files/app.config", "etc/app.config"}, 47 | + {template, "files/vm.args", "etc/vm.args"} 48 | ]} 49 | ]. 50 | -------------------------------------------------------------------------------- /pkgs/mongooseim/s2s-listener-certfile.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/ejabberd_s2s_in.erl b/apps/ejabberd/src/ejabberd_s2s_in.erl 2 | index c746587..7f48c56 100644 3 | --- a/apps/ejabberd/src/ejabberd_s2s_in.erl 4 | +++ b/apps/ejabberd/src/ejabberd_s2s_in.erl 5 | @@ -153,6 +153,10 @@ init([{SockMod, Socket}, Opts]) -> 6 | {value, {_, S}} -> S; 7 | _ -> none 8 | end, 9 | + CertListenerOpts = case lists:keyfind(certfile, 1, Opts) of 10 | + false -> []; 11 | + Found -> [Found] 12 | + end, 13 | {StartTLS, TLSRequired, TLSCertverify} = case ejabberd_config:get_local_option(s2s_use_starttls) of 14 | UseTls when (UseTls==undefined) or (UseTls==false) -> 15 | {false, false, false}; 16 | @@ -179,7 +183,7 @@ init([{SockMod, Socket}, Opts]) -> 17 | tls_enabled = false, 18 | tls_required = TLSRequired, 19 | tls_certverify = TLSCertverify, 20 | - tls_options = TLSOpts, 21 | + tls_options = TLSOpts ++ CertListenerOpts, 22 | timer = Timer}}. 23 | 24 | %%---------------------------------------------------------------------- 25 | -------------------------------------------------------------------------------- /pkgs/mongooseim/systemd.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/ejabberd_app.erl b/apps/ejabberd/src/ejabberd_app.erl 2 | index 118cb4bb..e632c14b 100644 3 | --- a/apps/ejabberd/src/ejabberd_app.erl 4 | +++ b/apps/ejabberd/src/ejabberd_app.erl 5 | @@ -71,7 +71,11 @@ start(normal, _Args) -> 6 | mongoose_metrics:init(), 7 | ejabberd_listener:start_listeners(), 8 | ejabberd_admin:start(), 9 | - ?INFO_MSG("ejabberd ~s is started in the node ~p", [?VERSION, node()]), 10 | + Message = io_lib:format( 11 | + "MongooseIM ~s has finished starting up on node ~p", 12 | + [?VERSION, node()] 13 | + ), 14 | + sd_notify:sd_notify(0, "READY=1\nSTATUS=" ++ Message ++ "\n"), 15 | Sup; 16 | start(_, _) -> 17 | {error, badarg}. 18 | diff --git a/rebar.config b/rebar.config 19 | index 125fc1a1..ad677802 100644 20 | --- a/rebar.config 21 | +++ b/rebar.config 22 | @@ -45,7 +45,8 @@ 23 | {ecoveralls, ".*", {git, "git://github.com/nifoc/ecoveralls.git", "0e52c47"}}, 24 | {edown, ".*", {git, "git://github.com/uwiger/edown.git", {tag, "0.8"}}}, 25 | {mustache, ".*", {git, "git://github.com/mojombo/mustache.erl.git", "031c7aa"}}, 26 | - {recon, ".*", {git, "git://github.com/ferd/recon.git", "2.3.2"}} 27 | + {recon, ".*", {git, "git://github.com/ferd/recon.git", "2.3.2"}}, 28 | + {sd_notify, "1", {git, "git://github.com/systemd/erlang-sd_notify.git", {branch, "master"}}} 29 | ]}. 30 | 31 | {pre_hooks, [{compile, "tools/compile_riak_pb.sh"}]}. 32 | diff --git a/rel/reltool.config.script b/rel/reltool.config.script 33 | index 4066eceb..95ff149d 100644 34 | --- a/rel/reltool.config.script 35 | +++ b/rel/reltool.config.script 36 | @@ -74,6 +74,7 @@ BaseAppsToInclude = AppsToRun ++ 37 | alarms, 38 | idna, 39 | recon, 40 | + sd_notify, 41 | poolboy, 42 | uuid, 43 | setup, 44 | -------------------------------------------------------------------------------- /pkgs/mongooseim/tests/ctl-path.patch: -------------------------------------------------------------------------------- 1 | diff --git a/tests/distributed_helper.erl b/tests/distributed_helper.erl 2 | index 1f25af16..b13c674b 100644 3 | --- a/tests/distributed_helper.erl 4 | +++ b/tests/distributed_helper.erl 5 | @@ -33,8 +33,9 @@ remove_node_from_cluster(Node, _Config) -> 6 | verify_result(Node, remove), 7 | ok. 8 | 9 | -ctl_path(Node, Config) -> 10 | - script_path(Node, Config, "mongooseimctl"). 11 | +ctl_path(Node, _) -> 12 | + rpc:call(Node, os, find_executable, ["mongooseimctl", 13 | + "/run/current-system/sw/bin"]). 14 | 15 | script_path(Node, Config, Script) -> 16 | filename:join([get_cwd(Node, Config), "bin", Script]). 17 | diff --git a/tests/ejabberd_node_utils.erl b/tests/ejabberd_node_utils.erl 18 | index 3cb2c3d3..5d185876 100644 19 | --- a/tests/ejabberd_node_utils.erl 20 | +++ b/tests/ejabberd_node_utils.erl 21 | @@ -44,8 +44,9 @@ config_template_path(Node, Config) -> 22 | config_vars_path(Node, Config, File) -> 23 | filename:join([cwd(Node, Config), "..", "..", "rel", File]). 24 | 25 | -ctl_path(Node, Config) -> 26 | - filename:join([cwd(Node, Config), "bin", "mongooseimctl"]). 27 | +ctl_path(Node, _) -> 28 | + call_fun(Node, os, find_executable, ["mongooseimctl", 29 | + "/run/current-system/sw/bin"]). 30 | 31 | -type ct_config() :: list({Key :: term(), Value :: term()}). 32 | 33 | diff --git a/tests/reload_helper.erl b/tests/reload_helper.erl 34 | index 9a709d05..4b92f72a 100644 35 | --- a/tests/reload_helper.erl 36 | +++ b/tests/reload_helper.erl 37 | @@ -78,7 +78,9 @@ node_cfg(N, backup, C) -> flat([node_cwd(N, C), "etc", "ejabberd.cfg.bak"]); 38 | node_cfg(N, template, C) -> flat([node_cwd(N, C), "..", "..", "rel", "files", "ejabberd.cfg"]); 39 | node_cfg(N, vars, C) -> flat([node_cwd(N, C), "..", "..", "rel", "vars.config"]). 40 | 41 | -node_ctl(N, C) -> flat([node_cwd(N, C), "bin", "mongooseimctl"]). 42 | +node_ctl(N, _) -> 43 | + rpc:call(N, os, find_executable, ["mongooseimctl", 44 | + "/run/current-system/sw/bin"]). 45 | 46 | flat(PathComponents) -> filename:join(PathComponents). 47 | 48 | -------------------------------------------------------------------------------- /pkgs/mongooseim/tests/default.nix: -------------------------------------------------------------------------------- 1 | { lib, buildErlang, fetchFromGitHub, erlangPackages, mongooseim }: 2 | 3 | buildErlang rec { 4 | name = "ejabberd_tests"; 5 | 6 | inherit (mongooseim) src version; 7 | 8 | sourceRoot = "${src.name}/test/ejabberd_tests"; 9 | 10 | patches = [ 11 | ./ctl-path.patch ./dont-tamper-with-config.patch ./no-fed-node.patch 12 | ]; 13 | 14 | postPatch = '' 15 | # Remove tests that require changing cluster settings or modifying 16 | # configuration files. 17 | ${lib.concatMapStrings (suite: '' 18 | sed -i -e '/^ *{ *suites *,[^,]*, *'${ 19 | lib.escapeShellArg "${suite}_SUITE" 20 | }' *} *\. *$/d' *.spec 21 | '') [ 22 | "cluster_commands" "conf-reload" "connect" "metrics_api" "users_api" 23 | ]} 24 | 25 | # Increase timeouts for a few escalus wait_for_stanza calls: 26 | sed -i -e '/wait_for_stanza/s/10000/&0/' tests/s2s_SUITE.erl 27 | sed -i -e '/wait_for_stanza/s/5000/&0/' tests/amp_SUITE.erl 28 | ''; 29 | 30 | postBuild = '' 31 | erlc -o ebin run_common_test.erl 32 | ''; 33 | 34 | erlangDeps = with erlangPackages; [ 35 | cowboy erlsh escalus exml jiffy katt mustache proper shotgun usec 36 | ]; 37 | 38 | postInstall = '' 39 | mkdir -p "$out/etc" 40 | cp test.config roster.template *.spec "$out/etc/" 41 | cp -a tests "$out" 42 | ''; 43 | } 44 | -------------------------------------------------------------------------------- /pkgs/mongooseim/tests/dont-tamper-with-config.patch: -------------------------------------------------------------------------------- 1 | diff --git a/tests/component_SUITE.erl b/tests/component_SUITE.erl 2 | index d6b9b5ea..1c29338b 100644 3 | --- a/tests/component_SUITE.erl 4 | +++ b/tests/component_SUITE.erl 5 | @@ -37,15 +37,11 @@ 6 | 7 | all() -> 8 | [{group, xep0114_tcp}, 9 | - {group, xep0114_ws}, 10 | - {group, subdomain}, 11 | - {group, distributed}]. 12 | + {group, xep0114_ws}]. 13 | 14 | groups() -> 15 | [{xep0114_tcp, [], xep0114_tests()}, 16 | - {xep0114_ws, [], xep0114_tests()}, 17 | - {subdomain, [], [register_subdomain]}, 18 | - {distributed, [], [register_in_cluster, register_same_on_both]}]. 19 | + {xep0114_ws, [], xep0114_tests()}]. 20 | 21 | suite() -> 22 | escalus:suite(). 23 | -------------------------------------------------------------------------------- /pkgs/mongooseim/tests/no-fed-node.patch: -------------------------------------------------------------------------------- 1 | diff --git a/tests/s2s_SUITE.erl b/tests/s2s_SUITE.erl 2 | index 77d3ea46..e0ee7733 100644 3 | --- a/tests/s2s_SUITE.erl 4 | +++ b/tests/s2s_SUITE.erl 5 | @@ -75,7 +75,7 @@ suite() -> 6 | 7 | require_s2s_nodes() -> 8 | [{require, mim_node, {hosts, mim, node}}, 9 | - {require, fed_node, {hosts, fed, node}}]. 10 | + {require, fed_node, {hosts, mim2, node}}]. 11 | 12 | %%%=================================================================== 13 | %%% Init & teardown 14 | -------------------------------------------------------------------------------- /pkgs/mongooseim/tls-fixes.patch: -------------------------------------------------------------------------------- 1 | diff --git a/apps/ejabberd/src/ejabberd_s2s_in.erl b/apps/ejabberd/src/ejabberd_s2s_in.erl 2 | index 5e47bdf2..4b0bd925 100644 3 | --- a/apps/ejabberd/src/ejabberd_s2s_in.erl 4 | +++ b/apps/ejabberd/src/ejabberd_s2s_in.erl 5 | @@ -170,6 +170,7 @@ init([{SockMod, Socket}, Opts]) -> 6 | end, 7 | TLSOpts2 = lists:filter(fun({protocol_options, _}) -> true; 8 | ({dhfile, _}) -> true; 9 | + ({ciphers, _}) -> true; 10 | (_) -> false 11 | end, Opts), 12 | TLSOpts = lists:append(TLSOpts1, TLSOpts2), 13 | diff --git a/apps/ejabberd/src/ejabberd_socket.erl b/apps/ejabberd/src/ejabberd_socket.erl 14 | index 65a97b86..f84b2731 100644 15 | --- a/apps/ejabberd/src/ejabberd_socket.erl 16 | +++ b/apps/ejabberd/src/ejabberd_socket.erl 17 | @@ -158,16 +158,28 @@ connect(Addr, Port, Opts, Timeout) -> 18 | end. 19 | 20 | 21 | +-spec tcp_to_tls(inet:socket(), list()) -> fast_tls:tls_socket(). 22 | +tcp_to_tls(InetSock, TLSOpts) -> 23 | + SanitizedTLSOpts = case lists:keyfind(protocol_options, 1, TLSOpts) of 24 | + false -> TLSOpts; 25 | + {_, ProtoOpts} -> 26 | + NewProtoOpts = {protocol_options, string:join(ProtoOpts, "|")}, 27 | + lists:keyreplace(protocol_options, 1, TLSOpts, NewProtoOpts) 28 | + end, 29 | + {ok, TLSSocket} = fast_tls:tcp_to_tls(InetSock, SanitizedTLSOpts), 30 | + TLSSocket. 31 | + 32 | + 33 | -spec starttls(socket_state(), list()) -> socket_state(). 34 | starttls(SocketData, TLSOpts) -> 35 | - {ok, TLSSocket} = fast_tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts), 36 | + TLSSocket = tcp_to_tls(SocketData#socket_state.socket, TLSOpts), 37 | ejabberd_receiver:starttls(SocketData#socket_state.receiver, TLSSocket), 38 | SocketData#socket_state{socket = TLSSocket, sockmod = fast_tls}. 39 | 40 | 41 | -spec starttls(socket_state(), _, _) -> socket_state(). 42 | starttls(SocketData, TLSOpts, Data) -> 43 | - {ok, TLSSocket} = fast_tls:tcp_to_tls(SocketData#socket_state.socket, TLSOpts), 44 | + TLSSocket = tcp_to_tls(SocketData#socket_state.socket, TLSOpts), 45 | ejabberd_receiver:starttls(SocketData#socket_state.receiver, TLSSocket), 46 | send(SocketData, Data), 47 | SocketData#socket_state{socket = TLSSocket, sockmod = fast_tls}. 48 | -------------------------------------------------------------------------------- /pkgs/nexus/Nexus/DNS.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | -- | Module for manipulating/writing DNS zone files. 3 | module Nexus.DNS 4 | ( module Nexus.DNS.Types 5 | 6 | , mkSOA 7 | , mkTinySOA 8 | , mkTempSOA 9 | , mkRR 10 | 11 | , mkZone 12 | , mkTinyZone 13 | , mkTempZone 14 | 15 | , updateRecords 16 | 17 | , renderZone 18 | , checkZone 19 | ) where 20 | 21 | import Control.Lens ((%~), (^.), (^..)) 22 | import Data.ByteString.Char8 (unpack) 23 | import Data.Data (Data(toConstr)) 24 | import Data.Function (on) 25 | import Data.List (unionBy) 26 | 27 | import Nexus.DNS.Types 28 | import Nexus.DNS.ZoneBuilder 29 | 30 | -- | Create a SOA record with some defaults as recommended by 31 | -- . 32 | mkSOA :: (Domain a, Domain b) => a -> b -> SOARecord 33 | mkSOA primary email = SOARecord 34 | { _soaPrimary = toRRName primary 35 | , _soaEmail = toRRName email 36 | , _soaSerial = 0 37 | , _soaRefresh = 86400 38 | , _soaRetry = 7200 39 | , _soaExpire = 3600000 40 | , _soaNXDomainTTL = 172800 41 | } 42 | 43 | -- | Create a SOA record that's tailored for frequently updated zones. 44 | mkTinySOA :: (Domain a, Domain b) => a -> b -> SOARecord 45 | mkTinySOA primary email = (mkSOA primary email) 46 | { _soaRefresh = 60 47 | , _soaRetry = 60 48 | , _soaExpire = 14400 49 | , _soaNXDomainTTL = 0 50 | } 51 | 52 | -- | Create a SOA record that's tailored for short-lived zones. 53 | mkTempSOA :: (Domain a, Domain b) => a -> b -> SOARecord 54 | mkTempSOA primary email = (mkSOA primary email) 55 | { _soaRefresh = 0 56 | , _soaRetry = 0 57 | , _soaExpire = 0 58 | , _soaNXDomainTTL = 0 59 | } 60 | 61 | -- | Create a 'ResourceRecord' for the zone's $ORIGIN and with the default TTL. 62 | mkRR :: Record -> ResourceRecord 63 | mkRR record = ResourceRecord 64 | { _rrName = Origin 65 | , _rrTTL = Nothing 66 | , _rrRecord = record 67 | } 68 | 69 | mkZoneWith :: (RRName -> a -> SOARecord) 70 | -> FQDN 71 | -> a 72 | -> [ResourceRecord] 73 | -> Zone 74 | mkZoneWith soaFun fqdn email records = Zone 75 | { _zoneDomain = fqdn 76 | , _zoneTTL = _soaNXDomainTTL soa 77 | , _zoneSOA = soa 78 | , _zoneRecords = records 79 | } 80 | where 81 | nsRecs = records ^.. traverse . rrRecord . _Nameserver 82 | primary = head $ nsRecs ++ [mappend (toRRName fqdn) "ns1"] 83 | soa = soaFun primary email 84 | 85 | -- | Create a new 'Zone' with the same default values as in `mkSOA`. 86 | mkZone :: Domain a 87 | => FQDN -- ^ The FQDN of the zone 88 | -> a -- ^ The email address of the zone owner 89 | -> [ResourceRecord] -- ^ Initial resource records. The first 90 | -- 'Nameserver' entry will be used for the primary 91 | -- nameserver of the @SOA@ record. If there is no 92 | -- such entry, it will be @ns1.FQDN@. 93 | -> Zone 94 | mkZone = mkZoneWith mkSOA 95 | 96 | -- | Alias of mkZone creating a SOA record like in `mkTinySOA`. 97 | mkTinyZone :: Domain a => FQDN -> a -> [ResourceRecord] -> Zone 98 | mkTinyZone = mkZoneWith mkTinySOA 99 | 100 | -- | Alias of mkZone creating a SOA record like in `mkTempSOA`. 101 | mkTempZone :: Domain a => FQDN -> a -> [ResourceRecord] -> Zone 102 | mkTempZone = mkZoneWith mkTempSOA 103 | 104 | -- | Update the records of a 'Zone' that have the same 'rrName' as the ones 105 | -- specified in the first argument and increment the serial by one. 106 | -- 107 | -- If a record doesn't exist in the 'Zone' it's added. 108 | updateRecords :: [ResourceRecord] -> Zone -> Zone 109 | updateRecords newRRs = 110 | (zoneSOA . soaSerial %~ succ) . (zoneRecords %~ updateRecs) 111 | where 112 | updateRecs = unionBy ((==) `on` match) newRRs 113 | match rr = (_rrName rr, toConstr $ _rrRecord rr) 114 | 115 | -- | Check whether the zone and its records are valid. 116 | -- 117 | -- Returns 'Nothing' if everything is fine or 'Just' with an error string. 118 | checkZone :: Zone -> Maybe String 119 | checkZone z = 120 | case nameservers of 121 | [] -> Just $ "No nameservers found for " ++ show domain ++ "." 122 | _ -> Nothing 123 | where 124 | domain = unpack $ toByteString $ z ^. zoneDomain 125 | nameservers = z ^. zoneRecords ^.. traverse . rrRecord . _Nameserver 126 | -------------------------------------------------------------------------------- /pkgs/nexus/Nexus/DNS/Types.hs: -------------------------------------------------------------------------------- 1 | -- | Module for manipulating/writing DNS zone files. 2 | {-# LANGUAGE DeriveGeneric, DeriveDataTypeable, TypeFamilies #-} 3 | {-# LANGUAGE TemplateHaskell, GeneralizedNewtypeDeriving, DeriveAnyClass #-} 4 | module Nexus.DNS.Types 5 | ( module Nexus.DNS.Types.Domain 6 | , module Nexus.DNS.Types.IpAddr 7 | , module Nexus.DNS.Types.NatInt32 8 | 9 | , Record(..) 10 | , AsRecord(..) 11 | 12 | , ResourceRecord(..) 13 | , HasResourceRecord(..) 14 | 15 | , SOARecord(..) 16 | , HasSOARecord(..) 17 | 18 | , Zone(..) 19 | , HasZone(..) 20 | ) where 21 | 22 | import Control.Lens (makeClassy, makeClassyPrisms) 23 | import Data.ByteString (ByteString) 24 | import Data.Data (Data(..)) 25 | import Data.Maybe (catMaybes) 26 | import Data.Serialize (Serialize) 27 | import Data.Typeable (Typeable) 28 | import Data.Word (Word16, Word32) 29 | import GHC.Generics (Generic) 30 | 31 | import qualified Data.SafeCopy as SC 32 | import qualified Data.Text as T 33 | import qualified Data.Text.Encoding as TE 34 | 35 | import Nexus.DNS.Types.Domain 36 | import Nexus.DNS.Types.IpAddr 37 | import Nexus.DNS.Types.NatInt32 38 | 39 | -- | This represents a zone file typically used in BIND or NSD. 40 | data Zone = Zone 41 | { _zoneDomain :: FQDN -- ^ The FQDN of the zone file 42 | , _zoneTTL :: Word32 -- ^ The default time-to-live in seconds 43 | , _zoneSOA :: SOARecord -- ^ The Start of Authority record 44 | , _zoneRecords :: [ResourceRecord] -- ^ All the resource records of the zone 45 | } deriving (Show, Serialize, Typeable, Generic) 46 | 47 | -- | The DNS record type and its data as an ADT. 48 | data Record 49 | = IPv4Address IPv4 -- ^ @A@ record 50 | | IPv6Address IPv6 -- ^ @AAAA@ record 51 | | TextRecord ByteString -- ^ @TXT@ record 52 | | CanonicalName RRName -- ^ @CNAME@ record 53 | | DelegationName RRName -- ^ @DNAME@ record 54 | | MailExchange Word16 RRName -- ^ @MX@ record 55 | | Nameserver RRName -- ^ @NS@ record 56 | | Pointer RRName -- ^ @PTR@ record 57 | | ServiceLocator Word16 Word16 Word16 RRName -- ^ @SRV@ record 58 | deriving (Show, Data, Serialize, Typeable, Generic) 59 | 60 | -- | A DNS resource record with the most common fields. 61 | -- 62 | -- The record class is not included here, because we're only interested in 63 | -- Internet (@IN@) records. 64 | -- 65 | -- The record type and data are both encoded in 'Record'. 66 | data ResourceRecord = ResourceRecord 67 | { _rrName :: RRName -- ^ The resource record name 68 | , _rrTTL :: Maybe Word32 -- ^ The time-to-live in seconds or the zone-wide 69 | -- default if 'Nothing' 70 | , _rrRecord :: Record -- ^ The record type and data 71 | } deriving (Show, Serialize, Typeable, Generic) 72 | 73 | -- | The Start of Authority record. 74 | data SOARecord = SOARecord { 75 | -- | The primary master of the 'Zone' 76 | _soaPrimary :: RRName, 77 | -- | Mail address of the one responsible 78 | _soaEmail :: RRName, 79 | -- | The serial number of the 'Zone' 80 | _soaSerial :: Word32, 81 | -- | The time interval before the 'Zone' should be refreshed 82 | _soaRefresh :: NatInt32, 83 | -- | The time interval that should elapse before a failed refresh should be 84 | -- retried 85 | _soaRetry :: NatInt32, 86 | -- | The upper limit on the time interval that can elapse before the zone 87 | -- is no longer authoritative 88 | _soaExpire :: NatInt32, 89 | -- | The caching time for NXDOMAIN errors 90 | _soaNXDomainTTL :: Word32 91 | } deriving (Show, Serialize, Typeable, Generic) 92 | 93 | -- | Lenses for 'Zone' 94 | makeClassy ''Zone 95 | 96 | -- | Lenses for 'ResourceRecord' 97 | makeClassy ''ResourceRecord 98 | 99 | -- | Lenses for 'SOARecord' 100 | makeClassy ''SOARecord 101 | 102 | -- | Prisms for 'Record' 103 | makeClassyPrisms ''Record 104 | 105 | -- | Obsolete version of 'Zone' for safecopy migrations. 106 | data Zone_v0 = Zone_v0 107 | { zone_v0_FQDN :: [T.Text] 108 | , zone_v0_Email :: [T.Text] 109 | , zone_v0_Nameservers :: [[T.Text]] 110 | , zone_v0_Serial :: Word32 111 | , zone_v0_IPv4Address :: Maybe IPv4 112 | , zone_v0_IPv6Address :: Maybe IPv6 113 | } deriving (Typeable, Generic) 114 | 115 | instance SC.Migrate Zone where 116 | type MigrateFrom Zone = Zone_v0 117 | migrate oldZone = Zone 118 | { _zoneDomain = migrateFQDN $ zone_v0_FQDN oldZone 119 | , _zoneTTL = 0 120 | , _zoneSOA = SOARecord 121 | { _soaPrimary = toRRName $ head nameservers 122 | , _soaEmail = toRRName email 123 | , _soaSerial = zone_v0_Serial oldZone 124 | , _soaRefresh = 60 125 | , _soaRetry = 60 126 | , _soaExpire = 14400 127 | , _soaNXDomainTTL = 0 128 | } 129 | , _zoneRecords = fmap (ResourceRecord Origin Nothing) $ 130 | (Nameserver . toRRName <$> nameservers) ++ addrRRs 131 | } 132 | where 133 | oldFqdnToBS = TE.encodeUtf8 . T.intercalate (T.singleton '.') 134 | migrateFQDN = either error id . parseDomain . oldFqdnToBS 135 | nameservers = migrateFQDN <$> zone_v0_Nameservers oldZone 136 | email = migrateFQDN $ zone_v0_Email oldZone 137 | addrRRs = catMaybes [ IPv4Address <$> zone_v0_IPv4Address oldZone 138 | , IPv6Address <$> zone_v0_IPv6Address oldZone 139 | ] 140 | 141 | SC.deriveSafeCopy 0 'SC.base ''Record 142 | SC.deriveSafeCopy 0 'SC.base ''ResourceRecord 143 | SC.deriveSafeCopy 0 'SC.base ''SOARecord 144 | SC.deriveSafeCopy 0 'SC.base ''Zone_v0 145 | SC.deriveSafeCopy 1 'SC.extension ''Zone 146 | -------------------------------------------------------------------------------- /pkgs/nexus/Nexus/DNS/Types/IpAddr.hs: -------------------------------------------------------------------------------- 1 | -- | Parsing and validation of IP addresses. 2 | {-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, DeriveGeneric #-} 3 | module Nexus.DNS.Types.IpAddr 4 | ( IPv4 5 | , mkIPv4 6 | , ip4toByteString 7 | , mkPublicIPv4 8 | , isPublicIPv4 9 | 10 | , IPv6 11 | , mkIPv6 12 | , ip6toByteString 13 | , mkPublicIPv6 14 | , isPublicIPv6 15 | ) where 16 | 17 | import Control.Monad (mfilter) 18 | import Data.ByteString.Char8 (ByteString, unpack, pack) 19 | import Data.Data (Data(..)) 20 | import Data.String (IsString(..)) 21 | import Data.Typeable (Typeable) 22 | import GHC.Generics (Generic) 23 | import Text.Read (readMaybe) 24 | 25 | import qualified Data.IP as IP 26 | import qualified Data.SafeCopy as SC 27 | import qualified Data.Serialize as S 28 | 29 | -- | An IP version 4 address 30 | newtype IPv4 = IPv4 IP.IPv4 31 | deriving (Show, Eq, Bounded, Enum, Data, Typeable, Generic) 32 | 33 | instance SC.SafeCopy IPv4 where 34 | putCopy (IPv4 i) = SC.contain . SC.safePut $ IP.fromIPv4 i 35 | getCopy = SC.contain $ fmap (IPv4 . IP.toIPv4) SC.safeGet 36 | 37 | instance S.Serialize IPv4 where 38 | put (IPv4 i) = S.put $ IP.fromIPv4 i 39 | get = fmap (IPv4 . IP.toIPv4) S.get 40 | 41 | instance IsString IPv4 where 42 | fromString = IPv4 . fromString 43 | 44 | -- | Parse an 'IPv4' address from the given 'ByteString'. 45 | mkIPv4 :: ByteString -> Maybe IPv4 46 | mkIPv4 = fmap IPv4 . readMaybe . unpack 47 | 48 | -- | Convert an 'IPv4' to a 'ByteString'. 49 | ip4toByteString :: IPv4 -> ByteString 50 | ip4toByteString (IPv4 i) = pack $ show i 51 | 52 | -- | Parse an 'IPv4' address like 'mkIPv4' and return Nothing if it's not a 53 | -- public address ('isPublicIPv4'). 54 | mkPublicIPv4 :: ByteString -> Maybe IPv4 55 | mkPublicIPv4 = mfilter isPublicIPv4 . mkIPv4 56 | 57 | nonPublicIPv4Ranges :: [IP.AddrRange IP.IPv4] 58 | nonPublicIPv4Ranges = map read 59 | -- From https://github.com/houseabsolute/Data-Validate-IP 60 | [ "127.0.0.0/8" -- loopback 61 | , "10.0.0.0/8" -- private 62 | , "172.16.0.0/12" -- private 63 | , "192.168.0.0/16" -- private 64 | , "192.0.2.0/24" -- test network 65 | , "198.51.100.0/24" -- test network 66 | , "203.0.113.0/24" -- test network 67 | , "192.88.99.0/24" -- anycast 68 | , "224.0.0.0/4" -- multicast 69 | , "169.254.0.0/16" -- link local 70 | , "0.0.0.0/8" -- unroutable 71 | , "100.64.0.0/10" -- unroutable 72 | , "192.0.0.0/29" -- unroutable 73 | , "198.18.0.0/15" -- unroutable 74 | , "240.0.0.0/4" -- unroutable 75 | ] 76 | 77 | -- | Check whether the given 'IPv4' is a valid public IP address. 78 | isPublicIPv4 :: IPv4 -> Bool 79 | isPublicIPv4 (IPv4 i) = not $ any (IP.isMatchedTo i) nonPublicIPv4Ranges 80 | 81 | -- | An IP version 6 address 82 | newtype IPv6 = IPv6 IP.IPv6 83 | deriving (Show, Eq, Bounded, Enum, Data, Typeable, Generic) 84 | 85 | instance SC.SafeCopy IPv6 where 86 | putCopy (IPv6 i) = SC.contain . SC.safePut $ IP.fromIPv6 i 87 | getCopy = SC.contain $ fmap (IPv6 . IP.toIPv6) SC.safeGet 88 | 89 | instance S.Serialize IPv6 where 90 | put (IPv6 i) = S.put $ IP.fromIPv6 i 91 | get = fmap (IPv6 . IP.toIPv6) S.get 92 | 93 | instance IsString IPv6 where 94 | fromString = IPv6 . fromString 95 | 96 | -- | Parse an 'IPv6' address from the given 'ByteString'. 97 | mkIPv6 :: ByteString -> Maybe IPv6 98 | mkIPv6 = fmap IPv6 . readMaybe . unpack 99 | 100 | -- | Convert an 'IPv6' to a 'ByteString'. 101 | ip6toByteString :: IPv6 -> ByteString 102 | ip6toByteString (IPv6 i) = pack $ show i 103 | 104 | -- | Parse an 'IPv6' address like 'mkIPv6' and return Nothing if it's not a 105 | -- public address ('isPublicIPv6'). 106 | mkPublicIPv6 :: ByteString -> Maybe IPv6 107 | mkPublicIPv6 = mfilter isPublicIPv6 . mkIPv6 108 | 109 | nonPublicIPv6Ranges :: [IP.AddrRange IP.IPv6] 110 | nonPublicIPv6Ranges = map read 111 | -- From https://github.com/houseabsolute/Data-Validate-IP 112 | [ "::1/128" -- loopback 113 | , "::/128" -- unroutable 114 | , "::ffff:0:0/96" -- IPv4 mapped 115 | , "100::/64" -- discard 116 | , "2001::/23" -- special 117 | , "2001::/32" -- teredo 118 | , "2001:10::/28" -- orchid 119 | , "2001:db8::/32" -- documentation 120 | , "fc00::/7" -- private 121 | , "fe80::/10" -- link local 122 | , "ff00::/8" -- multicast 123 | ] 124 | 125 | -- | Check whether the given 'IPv6' is a valid public IP address. 126 | isPublicIPv6 :: IPv6 -> Bool 127 | isPublicIPv6 (IPv6 i) = not $ any (IP.isMatchedTo i) nonPublicIPv6Ranges 128 | -------------------------------------------------------------------------------- /pkgs/nexus/Nexus/DNS/Types/NatInt32.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell, GeneralizedNewtypeDeriving #-} 2 | -- | This module implements a data type for natural 32-bit signed integers. 3 | -- 4 | -- This is what's used for the @REFRESH@, @RETRY@ and @EXPIRE@ fields as 5 | -- defined by . 6 | -- 7 | module Nexus.DNS.Types.NatInt32 (NatInt32(..)) where 8 | 9 | import Control.Arrow (first) 10 | import Control.Monad (liftM2) 11 | import Data.Int (Int32) 12 | import Data.Serialize (Serialize) 13 | import GHC.Exception (throw, ArithException(Underflow, Overflow)) 14 | 15 | import qualified Data.SafeCopy as SC 16 | 17 | -- | A positive 32-bit signed integer ranging from @0@ to @2147483647@ 18 | newtype NatInt32 = NatInt32 Int32 19 | deriving (Eq, Ord, Serialize) 20 | 21 | SC.deriveSafeCopy 0 'SC.base ''NatInt32 22 | 23 | instance Read NatInt32 where 24 | readsPrec d = fmap (first NatInt32) . filter (cond . fst) . readsPrec d 25 | where cond = liftM2 (&&) (>= minVal) (<= maxVal) 26 | 27 | instance Show NatInt32 where 28 | showsPrec d (NatInt32 n) = showsPrec d n 29 | 30 | instance Num NatInt32 where 31 | NatInt32 a + NatInt32 b = checkBounds (fromIntegral a + fromIntegral b) 32 | NatInt32 a * NatInt32 b = checkBounds (fromIntegral a * fromIntegral b) 33 | NatInt32 a - NatInt32 b = checkBounds (fromIntegral a - fromIntegral b) 34 | fromInteger = checkBounds 35 | signum (NatInt32 n) = NatInt32 (signum n) 36 | abs = id 37 | 38 | instance Real NatInt32 where 39 | toRational (NatInt32 n) = toRational n 40 | 41 | instance Enum NatInt32 where 42 | pred (NatInt32 0) = enumErr "pred" False 43 | pred (NatInt32 n) = NatInt32 $ pred n 44 | succ (NatInt32 n) | n == (maxBound :: Int32) = enumErr "succ" True 45 | | otherwise = NatInt32 $ succ n 46 | fromEnum (NatInt32 n) = fromEnum n 47 | toEnum n | n < minVal = enumErr "toEnum" False 48 | | n > maxVal = enumErr "toEnum" True 49 | | otherwise = NatInt32 (toEnum n) 50 | enumFrom (NatInt32 n) = NatInt32 <$> enumFrom n 51 | enumFromThen (NatInt32 a) (NatInt32 b) = NatInt32 <$> enumFromThen a b 52 | 53 | instance Bounded NatInt32 where 54 | minBound = NatInt32 0 55 | maxBound = NatInt32 (maxBound :: Int32) 56 | 57 | instance Integral NatInt32 where 58 | quotRem (NatInt32 a) (NatInt32 b) = (NatInt32 q, NatInt32 r) 59 | where (q, r) = quotRem a b 60 | toInteger (NatInt32 n) = toInteger n 61 | 62 | -- This is VERY expensive, but we're only using this for zone files, so it's 63 | -- less an issue and we care more about the constraints rather than speed. 64 | checkBounds :: Integer -> NatInt32 65 | checkBounds n | n < minVal = throw Underflow 66 | | n > maxVal = throw Overflow 67 | | otherwise = NatInt32 $ fromIntegral n 68 | 69 | minVal :: Integral n => n 70 | minVal = fromIntegral (minBound :: NatInt32) 71 | 72 | maxVal :: Integral n => n 73 | maxVal = fromIntegral (maxBound :: NatInt32) 74 | 75 | enumErr :: String -> Bool -> a 76 | enumErr fun minMax = 77 | errorWithoutStackTrace $ "Nexus.NatInt32." ++ fun ++ ": " ++ desc 78 | where 79 | desc = if minMax then "n > " ++ show (maxVal :: NatInt32) 80 | else "n < " ++ show (minVal :: NatInt32) 81 | -------------------------------------------------------------------------------- /pkgs/nexus/Nexus/DNS/ZoneBuilder.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TupleSections #-} 2 | -- | A module for rendering 'Zone' data. 3 | module Nexus.DNS.ZoneBuilder (renderZone) where 4 | 5 | import Data.Char (ord) 6 | import Data.ByteString (ByteString) 7 | import Data.ByteString.Builder 8 | import Data.Monoid ((<>)) 9 | import Data.Word (Word8) 10 | 11 | import qualified Data.ByteString.Builder.Prim as BP 12 | 13 | import Nexus.DNS.Types 14 | 15 | renderRType :: String -> [Builder] -> Builder 16 | renderRType _ [] = mempty 17 | renderRType t (b:bs) = string7 t <> char7 ' ' <> b 18 | <> mconcat (mappend (char7 ' ') <$> bs) 19 | <> char7 '\n' 20 | 21 | renderSOA :: SOARecord -> Builder 22 | renderSOA soa = string7 "@ IN " <> renderRType "SOA" 23 | [ renderDomain $ _soaPrimary soa 24 | , renderDomain $ _soaEmail soa 25 | , word32Dec $ _soaSerial soa 26 | , word32Dec . fromIntegral $ _soaRefresh soa 27 | , word32Dec . fromIntegral $ _soaRetry soa 28 | , word32Dec . fromIntegral $ _soaExpire soa 29 | , word32Dec $ _soaNXDomainTTL soa 30 | ] 31 | 32 | renderTXT :: ByteString -> Builder 33 | renderTXT txt = 34 | char7 '"' <> BP.primMapByteStringBounded escape txt <> char7 '"' 35 | where 36 | escape :: BP.BoundedPrim Word8 37 | escape = BP.condB (== c '\\') (fixed2 (c '\\', c '\\')) $ 38 | BP.condB (== c '"') (fixed2 (c '\\', c '"')) $ 39 | BP.condB isPrintable (BP.liftFixedToBounded BP.word8) 40 | decEsc 41 | 42 | isPrintable x = x >= 32 && x <= 126 43 | c = fromIntegral . ord 44 | boundedW8 = BP.liftFixedToBounded BP.word8 45 | fixed2 x = BP.liftFixedToBounded $ const x BP.>$< BP.word8 BP.>*< BP.word8 46 | decEsc = (c '\\',) BP.>$< boundedW8 BP.>*< BP.word8Dec 47 | 48 | renderRecord :: Record -> Builder 49 | renderRecord (IPv4Address ip) = 50 | renderRType "A" [byteString $ ip4toByteString ip] 51 | renderRecord (IPv6Address ip) = 52 | renderRType "AAAA" [byteString $ ip6toByteString ip] 53 | renderRecord (TextRecord text) = 54 | renderRType "TXT" [renderTXT text] 55 | renderRecord (CanonicalName domain) = 56 | renderRType "CNAME" [renderDomain domain] 57 | renderRecord (DelegationName domain) = 58 | renderRType "DNAME" [renderDomain domain] 59 | renderRecord (MailExchange pref domain) = 60 | renderRType "MX" [word16Dec pref, renderDomain domain] 61 | renderRecord (Nameserver domain) = 62 | renderRType "NS" [renderDomain domain] 63 | renderRecord (Pointer domain) = 64 | renderRType "PTR" [renderDomain domain] 65 | renderRecord (ServiceLocator prio weight port domain) = 66 | renderRType "SRV" [ word16Dec prio, word16Dec weight, word16Dec port 67 | , renderDomain domain 68 | ] 69 | 70 | renderRR :: ResourceRecord -> Builder 71 | renderRR (ResourceRecord domain ttl record) 72 | = renderDomain domain 73 | <> maybe mempty (mappend (char7 ' ') . word32Dec) ttl 74 | <> string7 " IN " 75 | <> renderRecord record 76 | 77 | -- | Render a zone file of the specified 'Zone' and return it as a 'Builder'. 78 | renderZone :: Zone -> Builder 79 | renderZone z = string7 "$TTL " <> word32Dec (_zoneTTL z) <> char7 '\n' 80 | <> renderSOA (_zoneSOA z) 81 | <> mconcat (fmap renderRR (_zoneRecords z)) 82 | -------------------------------------------------------------------------------- /pkgs/nexus/Nexus/Process.hs: -------------------------------------------------------------------------------- 1 | -- | Functions for interacting with system processes. 2 | module Nexus.Process (pipeToStdin) where 3 | 4 | import Control.Exception (bracket) 5 | import Data.ByteString.Char8 (ByteString, unpack) 6 | import System.Exit (ExitCode(..)) 7 | import System.IO (hClose) 8 | import System.Process 9 | 10 | import qualified Data.ByteString.Lazy as BL 11 | 12 | -- | Run a command and pipe a lazy 'BL.ByteString' into stdin. 13 | pipeToStdin :: FilePath -- ^ The command to execute 14 | -> [ByteString] -- ^ Arguments to pass to the command 15 | -> BL.ByteString -- ^ Data to write to the command 16 | -> IO a -- ^ Handler to call on success 17 | -> (Int -> IO a) -- ^ Handler to call on failure (with exit code) 18 | -> IO a 19 | pipeToStdin cmd args d onSuccess onFailure = do 20 | exitCode <- bracket (createProcess procCmd) cleanup process 21 | case exitCode of 22 | ExitSuccess -> onSuccess 23 | ExitFailure code -> onFailure code 24 | where 25 | procCmd = (proc cmd (unpack <$> args)) 26 | { std_in = CreatePipe 27 | , close_fds = True 28 | } 29 | cleanup (Just i, _, _, p) = hClose i >> terminateProcess p 30 | cleanup _ = return () 31 | 32 | process (Just i, _, _, p) = BL.hPutStr i d >> hClose i >> waitForProcess p 33 | process _ = return $ ExitFailure 126 34 | -------------------------------------------------------------------------------- /pkgs/nexus/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /pkgs/nexus/SocketTest.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric, OverloadedStrings #-} 2 | import Control.Concurrent (forkIO) 3 | import Control.Concurrent.Async (concurrently_) 4 | import Control.Concurrent.STM (atomically) 5 | import Control.Concurrent.STM.TChan 6 | import Control.Concurrent.STM.TSem 7 | import Control.Monad (void, unless) 8 | import Data.List (intercalate) 9 | import Data.Serialize (Serialize) 10 | import GHC.Generics (Generic) 11 | import System.IO (stderr) 12 | import System.Posix.Env (setEnv) 13 | import System.Posix.Process (getProcessID) 14 | 15 | import qualified Data.ByteString.Char8 as B 16 | import qualified Network.Socket as NS 17 | import qualified Network.Socket.ByteString as NSB 18 | 19 | import Nexus.Socket as Nexus 20 | 21 | data TestData = TestData Int String deriving (Show, Generic) 22 | instance Serialize TestData 23 | 24 | data Result = Result String deriving Show 25 | type ResultChan = TChan Result 26 | 27 | mkSocket :: Int -> IO Int 28 | mkSocket expect = do 29 | sock <- NS.socket NS.AF_UNIX NS.Stream 0 30 | NS.bind sock . NS.SockAddrUnix $ "test" ++ show expect ++ ".sock" 31 | NS.listen sock NS.maxListenQueue 32 | let fd = fromIntegral $ NS.fdSocket sock 33 | unless (expect == fd) . error $ "Expected socket FD to be " ++ show expect 34 | ++ " but it is " ++ show fd ++ " instead." 35 | return fd 36 | 37 | setupSystemdFds :: IO () 38 | setupSystemdFds = do 39 | sockfds <- mapM mkSocket [3, 4] 40 | writeEnv "LISTEN_PID" . show =<< getProcessID 41 | writeEnv "LISTEN_FDS" . show $ length sockfds 42 | writeEnv "LISTEN_FDNAMES" . intercalate ":" $ const "unknown" <$> sockfds 43 | where writeEnv n v = setEnv n v True 44 | 45 | writeResults :: ResultChan -> String -> IO () 46 | writeResults chan result = do 47 | B.hPutStrLn stderr . B.pack $ "WRITING RESULT: " ++ result 48 | atomically . writeTChan chan $ Result result 49 | 50 | serverHandler :: ResultChan -> TSem -> Connection TestData -> IO () 51 | serverHandler results sync conn = do 52 | val <- Nexus.recv conn 53 | case val of 54 | Just (TestData 1 "sent first") -> do 55 | writeResults results "server got first" 56 | True <- Nexus.broadcast conn $ TestData 2 "to all" 57 | writeResults results "sent to all" 58 | void $ Nexus.recv conn 59 | Just (TestData 3 "client after bogus") -> do 60 | writeResults results "packet after bogus client" 61 | atomically $ signalTSem sync 62 | Just (TestData 4 "client 2 ready") -> do 63 | writeResults results "client signalled ready" 64 | True <- Nexus.send conn (TestData 5 "client 2 go") 65 | void $ Nexus.recv conn 66 | Just _ -> writeResults results "unexpected data from client" 67 | Nothing -> do 68 | writeResults results "bogus packet from client" 69 | atomically $ signalTSem sync 70 | 71 | handler1 :: ResultChan -> TSem -> Connection TestData -> IO () 72 | handler1 results sync conn = do 73 | atomically $ waitTSem sync 74 | True <- Nexus.send conn $ TestData 1 "sent first" 75 | writeResults results "sent first" 76 | Just (TestData 2 "to all") <- Nexus.recv conn 77 | atomically $ waitTSem sync 78 | 79 | handler2 :: ResultChan -> TSem -> Connection TestData -> IO () 80 | handler2 results sync conn = do 81 | True <- Nexus.send conn $ TestData 4 "client 2 ready" 82 | Just (TestData 5 "client 2 go") <- Nexus.recv conn 83 | atomically $ signalTSem sync 84 | Just (TestData 2 "to all") <- Nexus.recv conn 85 | atomically $ signalTSem sync 86 | writeResults results "got broadcast" 87 | 88 | checkResults :: ResultChan -> IO () 89 | checkResults results = do 90 | expect "client signalled ready" 91 | expect "server got first" 92 | expect "sent first" 93 | expect "sent to all" 94 | expect "got broadcast" 95 | expect "bogus packet from client" 96 | expect "packet after bogus client" 97 | where 98 | expect x = do 99 | Result val <- atomically (readTChan results) 100 | if val == x 101 | then B.hPutStrLn stderr . B.pack $ 102 | "Got " ++ show val ++ " as expected (" ++ show x ++ ")." 103 | else error $ "Expected " ++ show x ++ " but got " 104 | ++ show val ++ " instead." 105 | 106 | task :: B.ByteString -> IO a -> IO () 107 | task desc fun = do 108 | B.hPutStrLn stderr $ B.append "+++ BEGIN: " desc 109 | void fun 110 | B.hPutStrLn stderr $ B.append "--- END: " desc 111 | 112 | main :: IO () 113 | main = do 114 | results <- newTChanIO 115 | sync <- atomically $ newTSem 0 116 | 117 | task "Set up systemd sockets" setupSystemdFds 118 | 119 | task "Fork off server" $ 120 | void . forkIO . Nexus.serve Nothing $ serverHandler results sync 121 | 122 | task "Run first wave of clients" $ do 123 | concurrently_ (Nexus.connectUnix "test3.sock" $ handler1 results sync) 124 | (Nexus.connectUnix "test4.sock" $ handler2 results sync) 125 | 126 | task "Run bogus client" $ do 127 | bogusSocket <- NS.socket NS.AF_UNIX NS.Stream 0 128 | NS.connect bogusSocket (NS.SockAddrUnix "test3.sock") 129 | NSB.sendAll bogusSocket "bogus" 130 | NS.close bogusSocket 131 | atomically $ waitTSem sync 132 | 133 | task "Run non-bogus client" $ 134 | Nexus.connectUnix "test3.sock" $ \conn -> do 135 | True <- Nexus.send conn $ TestData 3 "client after bogus" 136 | atomically $ waitTSem sync 137 | 138 | task "Collect results" $ 139 | checkResults results 140 | -------------------------------------------------------------------------------- /pkgs/nexus/default.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation, async, attoparsec, base, bytestring, cereal 2 | , iproute, lens, network, process, safecopy, stdenv, stm, systemd 3 | , template-haskell, text, unix 4 | }: 5 | mkDerivation { 6 | pname = "nexus"; 7 | version = "0.1.0.0"; 8 | src = ./.; 9 | libraryHaskellDepends = [ 10 | async attoparsec base bytestring cereal iproute lens network 11 | process safecopy stm systemd template-haskell text 12 | ]; 13 | testHaskellDepends = [ 14 | async base bytestring cereal network stm unix 15 | ]; 16 | license = stdenv.lib.licenses.agpl3; 17 | } 18 | -------------------------------------------------------------------------------- /pkgs/nexus/nexus.cabal: -------------------------------------------------------------------------------- 1 | name: nexus 2 | version: 0.1.0.0 3 | license: AGPL-3 4 | license-file: LICENSE 5 | author: aszlig 6 | maintainer: aszlig@redmoonstudios.org 7 | category: Network 8 | build-type: Simple 9 | cabal-version: >=1.10 10 | 11 | library 12 | ghc-options: -Wall 13 | exposed-modules: Nexus.DNS, 14 | Nexus.Process, 15 | Nexus.Socket 16 | other-modules: Nexus.DNS.Types, 17 | Nexus.DNS.Types.Domain, 18 | Nexus.DNS.Types.IpAddr, 19 | Nexus.DNS.Types.NatInt32, 20 | Nexus.DNS.ZoneBuilder 21 | build-depends: base == 4.*, 22 | async, 23 | attoparsec, 24 | bytestring, 25 | cereal, 26 | iproute, 27 | lens, 28 | network, 29 | process, 30 | safecopy, 31 | stm, 32 | systemd, 33 | template-haskell, 34 | text 35 | build-tools: hsc2hs 36 | default-language: Haskell2010 37 | 38 | test-suite nexus-test-socket 39 | ghc-options: -Wall 40 | type: exitcode-stdio-1.0 41 | main-is: SocketTest.hs 42 | build-depends: base == 4.*, 43 | async, 44 | bytestring, 45 | cereal, 46 | network, 47 | nexus, 48 | stm, 49 | unix 50 | default-language: Haskell2010 51 | -------------------------------------------------------------------------------- /pkgs/site/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchFromGitHub, haxe, haxePackages }: 2 | 3 | let 4 | hase = stdenv.mkDerivation rec { 5 | name = "hase"; 6 | version = "0.1.0"; 7 | 8 | src = fetchFromGitHub { 9 | owner = "aszlig"; 10 | repo = "hase"; 11 | rev = "76704ba163c3878fc18590d895d1a5d9e16538dd"; 12 | sha256 = "0f479fm38canqwjd8046k6l39pfln2gpm0anmn0pjjhg991svk4n"; 13 | }; 14 | 15 | installPhase = haxePackages.installLibHaxe { 16 | libname = "hase"; 17 | inherit version; 18 | files = "hase"; 19 | }; 20 | }; 21 | 22 | in stdenv.mkDerivation { 23 | name = "headcounter-site"; 24 | 25 | src = ./frontend; 26 | 27 | nativeBuildInputs = [ haxe ]; 28 | buildInputs = [ hase ]; 29 | 30 | buildPhase = '' 31 | haxe -main Headcounter -lib hase -js headcounter.js -dce full 32 | 33 | cat > index.html < 35 | Headcounter - coming soon 36 | 37 | 38 | 39 | 43 | HTML 44 | ''; 45 | 46 | installPhase = '' 47 | mkdir "$out" 48 | cp -t "$out" headcounter.js index.html 49 | ''; 50 | } 51 | -------------------------------------------------------------------------------- /pkgs/site/frontend/gfx/coming_soon.cat: -------------------------------------------------------------------------------- 1 | : plain : ansi : 2 | | __ / | RR R | 3 | |,' ,-. ,_._ , ,_ ,-/ |RR RRR RRRR R RR RRR | 4 | |`--, '-' | | | | | ) `-| |RRRR RRR R R R R R R RRR | 5 | | .-' | RRR | 6 | | .--, [TM] | RRRR KKKK | 7 | | `--. ,-. .-. ;-. | RRRR RRR RRR RRR | 8 | | .--' `-' `-' ' ' | RRRR RRR RRR R R | 9 | -------------------------------------------------------------------------------- /pkgs/site/frontend/gfx/head.cat: -------------------------------------------------------------------------------- 1 | : plain : ansi : 2 | | _...._ _...._ | BBBBBB BBBBBB | 3 | | .' .-'~ ~`-. `. | BB BBBB BBBB BB | 4 | | ( ( ) ) | B B B B | 5 | | ( `. .' ) | B BB BB B | 6 | | `. `., ,.' .' | BB BBB BBB BB | 7 | | `, .`-._ _.-'. ,' | BB ccccc ccccc BB | 8 | | |' _ ~ _ `| | cc K c K cc | 9 | | |.'*`. .'*`.| | cKKRKK KKRKKc | 10 | | |`-, '' `` ,-'| | cKKK KK KK KKKc | 11 | | | .-^-. | | c KKKKK c | 12 | | `-| `^' |-' | ccc KKK ccc | 13 | | |_.'. .'._| | ccccw wcccc | 14 | | ||vvv|| | cWWWWWc | 15 | | :.^^^.: | cWWWWWc | 16 | | ( ) | c c | 17 | -------------------------------------------------------------------------------- /pkgs/site/frontend/gfx/head_annoyed.cat: -------------------------------------------------------------------------------- 1 | : plain : ansi : 2 | | _...._ _...._ | BBBBBB BBBBBB | 3 | | .' .-'~ ~`-. `. | BB BBBB BBBB BB | 4 | | ( ( ) ) | B B B B | 5 | | ( `. .' ) | B BB BB B | 6 | | `. `., ,.' .' | BB BBB BBB BB | 7 | | `, .`-._ _.-'. ,' | BB ccccc ccccc BB | 8 | | |' : ~ : `| | cc r c r cc | 9 | | |.'#`. .'#`.| | cKKRKK KKRKKc | 10 | | |`-, '' `` ,-'| | cKKK KK KK KKKc | 11 | | | .-^-. | | c KKKKK c | 12 | | `-| `^' |-' : | ccc KKK ccc K | 13 | | : |_.'. .'._| : | K ccccw wcccc K | 14 | | : `.||vvv||,' : | K wwcWWWWWcww K | 15 | | : `.:. .:.' : | K wwcW Wcww K | 16 | | ^^^^^ | WWWWW | 17 | -- 18 | : plain : ansi : 19 | | _...._ _...._ | BBBBBB BBBBBB | 20 | | .' .-'~ ~`-. `. | BB BBBB BBBB BB | 21 | | ( ( ) ) | B B B B | 22 | | ( `. .' ) | B BB BB B | 23 | | `. `., ,.' .' | BB BBB BBB BB | 24 | | `, .`-._ _.-'. ,' | BB ccccc ccccc BB | 25 | | |' : ~ : `| | cc r c r cc | 26 | | |.'O`. .'O`.| | cKKRKK KKRKKc | 27 | | |` : '' `` : '| | cK r KK KK r Kc | 28 | | | .-^-. | . | c KKKKK c K | 29 | | `-| `^' |-' | ccc KKK ccc | 30 | | : |_.v v._| :: | K cccW Wccc XK | 31 | | :: `.||"""||,' : | KK wwcWWWWWcww K | 32 | | : `:. .:' : | K wcW Wcw K | 33 | | |^^^| | WWWWW | 34 | -- 35 | : plain : ansi : 36 | | _...._ _...._ | BBBBBB BBBBBB | 37 | | .' .-'~ ~`-. `. | BB BBBB BBBB BB | 38 | | ( ( ) ) | B B B B | 39 | | ( `. .' ) | B BB BB B | 40 | | `. `., ,.' .' | BB BBB BBB BB | 41 | | `, .`-._ _.-'. ,' | BB ccccc ccccc BB | 42 | | |' : ~ : `| | cc r c r cc | 43 | | |`-#-. .-#-'| | cKKRKK KKRKKc | 44 | | | " " | | c K K c | 45 | | | .-^-. | | c KKKKK c | 46 | | `-| `^' |-' | ccc KKK ccc | 47 | | : |_.v v._| :: | K cccW Wccc KK | 48 | | :: `.\ """ /,' :: | KK wwC WWW Cww KK | 49 | | : `.:|^^^|:.' : | K wwcWWWWWcww K | 50 | | | | 51 | -------------------------------------------------------------------------------- /pkgs/spectrum2/default.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchFromGitHub, fetchpatch, cmake, boost, swiften, popt, sqlite 2 | , libpqxx, pidgin, protobuf, dbus_glib, libev, libcommuni, curl, log4cxx 3 | , cppunit 4 | }: 5 | 6 | stdenv.mkDerivation { 7 | name = "spectrum2-git"; 8 | 9 | src = fetchFromGitHub { 10 | repo = "libtransport"; 11 | owner = "hanzz"; 12 | rev = "a319e79528df9f12cbba699384cd503e17714ca2"; 13 | sha256 = "01ssyzbdf5p84lvdy0957kyiv5rvlm97x7qsbwcj4yf6kajxlxbi"; 14 | }; 15 | 16 | patches = stdenv.lib.singleton (fetchpatch { 17 | url = "https://github.com/jpnurmi/libtransport/compare/libcommuni3.diff"; 18 | sha256 = "1d004n9czyidf93lk7ql4xj9d8xi1d35msbb3m11jsdirrkc9dgl"; 19 | }); 20 | 21 | postPatch = '' 22 | sed -i -e 's|/etc|'"$out"'/etc|' \ 23 | spectrum/src/CMakeLists.txt spectrum_manager/src/CMakeLists.txt 24 | ''; 25 | 26 | cmakeFlags = let 27 | mkFlags = name: let 28 | ucName = stdenv.lib.toUpper name; 29 | in [ 30 | "-DIRC_${ucName}_INCLUDE_DIR=${libcommuni}/include/Irc${name}" 31 | "-DIRC_${ucName}_LIBRARY=${libcommuni}/lib/libIrc${name}.so" 32 | ]; 33 | communiFlags = map mkFlags [ "Core" "Model" "Util" ]; 34 | in [ 35 | "-DENABLE_MYSQL=OFF" 36 | "-DENABLE_TESTS=ON" 37 | ] ++ stdenv.lib.flatten communiFlags; 38 | 39 | enableParallelBuilding = true; 40 | 41 | buildInputs = [ 42 | cmake boost swiften popt sqlite libpqxx pidgin 43 | protobuf dbus_glib libev libcommuni curl log4cxx 44 | cppunit 45 | ]; 46 | 47 | doInstallCheck = true; 48 | installCheckPhase = '' 49 | ./src/libtransport_test 50 | ''; 51 | } 52 | -------------------------------------------------------------------------------- /pkgs/spectrum2/libcommuni.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchFromGitHub, qt4 }: 2 | 3 | stdenv.mkDerivation rec { 4 | name = "libcommuni-${version}"; 5 | version = "3.3.0"; 6 | 7 | src = fetchFromGitHub { 8 | repo = "libcommuni"; 9 | owner = "communi"; 10 | rev = "v${version}"; 11 | sha256 = "1bsblr4rizgjin1p9igymcv40lbkm9p8f4h3k8pwdf6qfxdnzxsx"; 12 | }; 13 | 14 | postPatch = '' 15 | sed -i -e 's|/bin/pwd|pwd|g' -e 's/\/type -P/g' configure 16 | ''; 17 | 18 | propagatedBuildInputs = [ qt4 ]; 19 | 20 | doCheck = true; 21 | } 22 | -------------------------------------------------------------------------------- /pkgs/spectrum2/libpqxx.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchurl, python, postgresql }: 2 | 3 | stdenv.mkDerivation rec { 4 | name = "libpqxx-${version}"; 5 | version = "4.0.1"; 6 | 7 | src = fetchurl { 8 | url = "http://pqxx.org/download/software/libpqxx/${name}.tar.gz"; 9 | sha256 = "0f6wxspp6rx12fkasanb0z2g2gc8dhcfwnxagx8wwqbpg6ifsz09"; 10 | }; 11 | 12 | postPatch = '' 13 | sed -i -e 's|/bin/true|true|' configure 14 | sed -i -e 's|/usr/bin/python|${python}/bin/python|' tools/splitconfig 15 | ''; 16 | 17 | configureFlags = "--enable-shared"; 18 | 19 | propagatedBuildInputs = [ postgresql ]; 20 | } 21 | -------------------------------------------------------------------------------- /pkgs/spectrum2/swiften.nix: -------------------------------------------------------------------------------- 1 | { stdenv, fetchgit, pkgconfig, scons, boost, openssl, expat, libidn, zlib }: 2 | 3 | stdenv.mkDerivation rec { 4 | name = "swiften-${version}"; 5 | version = "3.0alpha"; 6 | 7 | src = fetchgit { 8 | url = "git://swift.im/swift"; 9 | rev = "refs/tags/swift-${version}"; 10 | sha256 = "0rblfwk33vg1bhy7dh4qkn1xy5nz48rlr5x4ychfyqrb05hrs7ak"; 11 | }; 12 | 13 | buildInputs = [ pkgconfig scons boost openssl expat libidn zlib ]; 14 | 15 | patches = [ ./swiften.patch ]; 16 | 17 | configurePhase = '' 18 | cat > config.py <Permalink/d' -e '/>Retest/d' -e '/Badge/,/<\/pre>/d' result.php 26 | ) > getreport.php 27 | sed -i -e 's/${reget "type"}/$argv[1]/g' \ 28 | -e 's/${reget "domain"}/$argv[2]/g' \ 29 | -e 's/\( "$out/bin/xmppoke-genreport" < "\$tmpdir/xmppoke/\$1.html" 49 | "${coreutils}/bin/cp" -r --no-preserve=all \ 50 | "$out/share/xmppoke-frontend/js" \ 51 | "$out/share/xmppoke-frontend/css" \ 52 | "$out/share/xmppoke-frontend/fonts" \ 53 | "$out/share/xmppoke-frontend/img" \ 54 | "\$tmpdir/xmppoke/" 55 | "${gnutar}/bin/tar" cf "\$1.tar" -C "\$tmpdir" xmppoke 56 | exit 0 57 | else 58 | echo "Unable to create temporary directory." >&2 59 | exit 1 60 | fi 61 | else 62 | echo "Usage: \$0 TYPE FQDN" >&2 63 | exit 1 64 | fi 65 | RUNNER 66 | chmod +x "$out/bin/xmppoke-genreport" 67 | ''; 68 | } 69 | -------------------------------------------------------------------------------- /pkgs/xmppoke/server-handler.patch: -------------------------------------------------------------------------------- 1 | diff --git a/net/server_select.lua b/net/server_select.lua 2 | index eff86e0..d01f50a 100644 3 | --- a/net/server_select.lua 4 | +++ b/net/server_select.lua 5 | @@ -450,7 +450,9 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 6 | out_put("server.lua: client", tostring(handler), " ", tostring(socket)); 7 | if not socket then 8 | disconnect( handler, "socket disappeared" ); 9 | - handler:close( true ) 10 | + if handler then 11 | + handler:close( true ) 12 | + end 13 | return false 14 | end 15 | local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern" 16 | -------------------------------------------------------------------------------- /release.nix: -------------------------------------------------------------------------------- 1 | { nixpkgs ? 2 | , system ? builtins.currentSystem 3 | }: 4 | 5 | let 6 | pkgs = import nixpkgs { inherit system; }; 7 | in { 8 | manual = with pkgs.lib; with builtins; let 9 | dummyOpts = let 10 | mkDummyOption = default: mkOption { 11 | type = types.unspecified; 12 | visible = false; 13 | description = "dummy option"; 14 | inherit default; 15 | }; 16 | in { 17 | users.extraUsers = mkDummyOption {}; 18 | networking.hostName = mkDummyOption "localhost"; 19 | }; 20 | 21 | modules = evalModules { 22 | modules = import ./modules/module-list.nix ++ singleton { 23 | options = dummyOpts; 24 | }; 25 | args = { 26 | pkgs = import ./pkgs { 27 | inherit pkgs; 28 | }; 29 | }; 30 | check = false; 31 | }; 32 | 33 | filterDoc = filter (opt: opt.visible && !opt.internal); 34 | optionsXML = toXML (filterDoc (optionAttrSetToDocList modules.options)); 35 | optionsFile = toFile "options.xml" (unsafeDiscardStringContext optionsXML); 36 | in pkgs.stdenv.mkDerivation { 37 | name = "headcounter-options"; 38 | 39 | buildInputs = singleton pkgs.libxslt; 40 | 41 | xsltFlags = '' 42 | --param section.autolabel 1 43 | --param section.label.includes.component.label 1 44 | --param html.stylesheet 'style.css' 45 | --param xref.with.number.and.title 1 46 | --param admon.style ''' 47 | ''; 48 | 49 | buildCommand = '' 50 | xsltproc -o options-db.xml \ 51 | ${} \ 52 | ${optionsFile} 53 | 54 | cat > manual.xml < 58 | Headcounter specific NixOS options 59 | 60 | The following NixOS options are specific to the Headcounter deployment 61 | 62 | 63 | 64 | XML 65 | 66 | xsltproc -o "$out/manual.html" $xsltFlags -nonet -xinclude \ 67 | ${pkgs.docbook5_xsl}/xml/xsl/docbook/xhtml/docbook.xsl \ 68 | manual.xml 69 | 70 | cp "${toPath "${nixpkgs}/nixos/doc/manual/style.css"}" "$out/style.css" 71 | 72 | mkdir -p "$out/nix-support" 73 | echo "doc manual $out manual.html" \ 74 | > "$out/nix-support/hydra-build-products" 75 | ''; 76 | }; 77 | 78 | tests = with pkgs.lib; let 79 | testsOnly = attrs: !attrs ? test; 80 | in mapAttrsRecursiveCond testsOnly (_: getAttr "test") (import ./tests { 81 | pkgs = import ./pkgs { inherit pkgs; }; 82 | inherit system; 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /ssl/snakeoil.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | 3 | let 4 | buildInputs = [ pkgs.openssl ]; 5 | 6 | rootCA = stdenv.mkDerivation { 7 | name = "snakeoil-ca-root"; 8 | inherit buildInputs; 9 | preferLocalBuild = true; 10 | buildCommand = '' 11 | mkdir -p "$out" 12 | openssl genrsa -out "$out/private.key" 2048 13 | openssl req -x509 -new -nodes -key "$out/private.key" -days 40000 \ 14 | -extensions v3_ca -out "$out/root.pem" <&2 94 | echo "intermediate certificate. This should not happen!" >&2 95 | exit 1 96 | fi 97 | 98 | cat > "$out" < 10 | gen_server:start({local, testclient}, ?MODULE, none, []). 11 | 12 | config() -> [ 13 | {escalus_user_db, xmpp}, 14 | {escalus_host, <<"server">>}, 15 | {escalus_server, <<"server">>}, 16 | {escalus_users, [ 17 | {user1, [{username, <<"user1">>}, 18 | {password, <<"testuser1">>}]}, 19 | {user2, [{username, <<"user2">>}, 20 | {password, <<"testuser2">>}]} 21 | ]} 22 | ]. 23 | 24 | init(_) -> 25 | {ok, _} = application:ensure_all_started(escalus), 26 | application:set_env(escalus, common_test, false), 27 | Config = escalus_event:start(escalus_cleaner:start(config())), 28 | {ok, #state{config = Config}}. 29 | 30 | handle_call(ping, _, State) -> 31 | {reply, pong, State}; 32 | 33 | handle_call(register, _, #state{config = Config} = State) -> 34 | Users = escalus_config:get_config(escalus_users, Config), 35 | Created = [escalus_users:create_user(Config, User) || User <- Users], 36 | lists:foreach(fun escalus_users:verify_creation/1, Created), 37 | NewConf = lists:keystore(escalus_users, 1, Config, {escalus_users, Users}), 38 | {reply, register_done, State#state{config = NewConf}}; 39 | 40 | handle_call(login, _, #state{config = Config} = State) -> 41 | Users = escalus_config:get_config(escalus_users, Config), 42 | Login = fun(Name) -> 43 | UserSpec = escalus_users:get_options(Config, Name), 44 | try 45 | {Name, escalus_client:start(Config, UserSpec, <<"test">>)} 46 | catch error:{assertion_failed, assert, is_iq_result, _, _} -> 47 | {Name, {error, login_failure}} 48 | end 49 | end, 50 | ConnectedUsers = lists:map(Login, proplists:get_keys(Users)), 51 | CheckLoggedIn = fun(C) -> 52 | Message = <<"Am I logged in?">>, 53 | OwnJID = escalus_client:full_jid(C), 54 | Stanza = escalus_stanza:chat_to(OwnJID, Message), 55 | ok = escalus_client:send(C, Stanza), 56 | try escalus_client:wait_for_stanza(C, 10000) of 57 | Result -> escalus_pred:is_chat_message(Message, Result) 58 | catch 59 | _:_ -> false 60 | end 61 | end, 62 | CheckConnection = fun({_, {ok, C}}) -> CheckLoggedIn(C); 63 | ({_, {error, _}}) -> false 64 | end, 65 | Status = case lists:all(CheckConnection, ConnectedUsers) of 66 | true -> logged_in; 67 | false -> login_failure 68 | end, 69 | OnlyConnected = fun({Name, {ok, C}}) -> {true, {Name, C}}; 70 | ({_, {error, _}}) -> false 71 | end, 72 | Extracted = lists:filtermap(OnlyConnected, ConnectedUsers), 73 | {reply, Status, State#state{users = Extracted}}; 74 | 75 | handle_call(communicate, _, #state{users = Users} = State) -> 76 | User1 = proplists:get_value(user1, Users), 77 | User2 = proplists:get_value(user2, Users), 78 | 79 | ChatTo = fun(From, To) -> 80 | JidTo = escalus_client:full_jid(To), 81 | Username = escalus_client:username(From), 82 | Message = <<"Hello from ", Username/binary>>, 83 | Stanza = escalus_stanza:chat_to(JidTo, Message), 84 | ok = escalus_client:send(User1, Stanza), 85 | Result = escalus_client:wait_for_stanza(To, 60000), 86 | true = escalus_pred:is_chat_message(Message, Result), 87 | ok 88 | end, 89 | 90 | ok = ChatTo(User1, User2), 91 | ok = ChatTo(User2, User1), 92 | 93 | {reply, great_communication, State}; 94 | 95 | handle_call(adhoc_ping, _, #state{users = Users} = State) -> 96 | User = proplists:get_value(user1, Users), 97 | Msg = <<"ping">>, 98 | PingStanza = escalus_stanza:adhoc_request(Msg), 99 | Stanza = escalus_stanza:to(PingStanza, <<"server">>), 100 | ok = escalus_client:send(User, Stanza), 101 | Response = escalus_client:wait_for_stanza(User, 60000), 102 | true = escalus_pred:is_adhoc_response(Msg, <<"completed">>, Response), 103 | Path = [{element, <<"command">>}, {element, <<"note">>}, cdata], 104 | Result = case exml_query:path(Response, Path) of 105 | <<"Pong">> -> pong; 106 | <<"Pang">> -> pang 107 | end, 108 | {reply, Result, State}; 109 | 110 | handle_call(check_connections, _, #state{users = Users} = State) -> 111 | IsConnected = fun(U) -> 112 | escalus_connection:is_connected(element(2, U)) 113 | end, 114 | Result = case lists:all(IsConnected, Users) of 115 | true -> still_connected; 116 | false -> not_connected_anymore 117 | end, 118 | {reply, Result, State}; 119 | 120 | handle_call(get_uptime, _, #state{users = Users} = State) -> 121 | User1 = proplists:get_value(user1, Users), 122 | ok = escalus_client:send(User1, escalus_stanza:last_activity(<<"server">>)), 123 | Reply = escalus_client:wait_for_stanza(User1, 60000), 124 | Result = exml_query:path(Reply, [ 125 | {element, <<"query">>}, 126 | {attr, <<"seconds">>} 127 | ]), 128 | {reply, list_to_integer(binary_to_list(Result)), State}; 129 | 130 | handle_call(_, _, State) -> 131 | {reply, wrong_call, State}. 132 | 133 | terminate(_, #state{config = Config}) -> 134 | escalus_cleaner:clean(Config). 135 | 136 | handle_cast(_, State) -> {noreply, State}. 137 | handle_info(_, State) -> {noreply, State}. 138 | code_change(_, State, _) -> {ok, State}. 139 | -------------------------------------------------------------------------------- /tests/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import ../pkgs {} 2 | , system ? builtins.currentSystem 3 | }: 4 | 5 | let 6 | callTest = fn: args: import fn ({ 7 | inherit (pkgs) lib; 8 | inherit pkgs system; 9 | } // args); 10 | in { 11 | acme = callTest ./acme.nix {}; 12 | code-reload = callTest ./code-reload {}; 13 | dyndns = callTest ./dyndns.nix {}; 14 | hclib = pkgs.callPackage ./hclib.nix {}; 15 | headcounter = callTest ./headcounter {}; 16 | mongooseim = callTest ./mongooseim {}; 17 | nsd-zone-writer = callTest ./nsd-zone-writer.nix {}; 18 | postfix = callTest ./postfix.nix {}; 19 | postgresql = callTest ./postgresql.nix {}; 20 | } 21 | -------------------------------------------------------------------------------- /tests/hclib.nix: -------------------------------------------------------------------------------- 1 | { runCommand, writeScript, nix, erlang }: 2 | 3 | { 4 | test = runCommand "test-run-hclib" { 5 | buildInputs = [ nix erlang ]; 6 | testAddresses = [ 7 | "-1.2.3.4" "10." "172.16." "198.168.0." "127.0.0.1." ":::" "f:::2" 8 | "0.0.0.0" "1.2.3.4" "253.252.251.250" "1.2.255.254" "::" "f::2" "::-1" 9 | "0.00.0.0" "0.0.000000000000.0" "0.256.0.1" "1.2.3.4.5" "256.255.65535" 10 | "00000000000037777777777" "0xffffffff" "0x12345678" "0x12.0x345678" 11 | "01000::" "::8:7:6:5:4:3:2:1" "8:7:6:5:4:3:2:1::" "8:7:6:5:4::3:2:1" 12 | "0x12.0X34.0x5678" "0x12.0X34.0x56.0X78" "255.0xFF.0177777" "4294967295" 13 | "255.255.255.0377" "255.16777215" "00377.0XFFFFFF" "255.255.65535" 14 | "4294967296" "0Xff.000000000377.0x0000ff.255" "0x100000000" "7:6:5:4:3::" 15 | "040000000000" "1.2.3.-4" "1.2.-3.4" "1.-2.3.4" "7:6:5:4::" 16 | "7:6:5:4:3:2::" "7:6:5:4:3:2:1::" "::1.256.3.4" "::-5.4.3.2" "::5.-4.3.2" 17 | "::5.4.-3.2" "::5.4.3.-2" "::FFFF:1.2.3.4.5" "::10." "::FFFF:172.16." 18 | "::5:4:3:2:1" "::6:5:4:3:2:1" "::7:6:5:4:3:2:1" "7::" "7:6::" "7:6:5::" 19 | "::g" "f:f11::10100:2" "f:f11::01100:2" "::17000" "::01700" "10000::" 20 | "c11:0c22:5c33::0088" "c11:0c22::0088" "::FFFF:1.2.255.254" 21 | "c11:0c22:5c33::55c0:c66c:77:0088" "c11:0c22:5c33:c440::c66c:77:0088" 22 | "c11:0c22:5c33::c66c:77:0088" "c11:0c22:5c33:c440::77:0088" 23 | "c11:0c22:5c33:c440:55c0::0088" "c11::55c0:c66c:77:0088" 24 | "c11:0c22:5c33:c440:55c0::77:0088" "c11:0c22:5c33:c440:55c0:c66c::0088" 25 | "c11:0c22:5c33:c440::0088" "c11::c66c:77:0088" "c11:0c22::77:0088" 26 | "c11:0c22::c66c:77:0088" "c11:0c22:5c33::77:0088" 27 | "c11:0c22:5c33:c440:55c0:c66c:77:0088" "c11::5c33:c440:55c0:c66c:77:0088" 28 | "c11:0c22::c440:55c0:c66c:77:0088" "c11::c440:55c0:c66c:77:0088" 29 | "c11:0c22::55c0:c66c:77:0088" "c11::77:0088" "f:f11::0100:2" "::17" 30 | "0700::" "::2:1" "::3:2:1" "::4:3:2:1" "fe80::198.168.0." 31 | "fec0::fFfF:127.0.0.1." "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" 32 | "10.0x019876" "012.01.054321" "0" "00" "0.0" "00.00.00" "::1.2.3.4.5" 33 | "::1.2.3.04" 34 | ]; 35 | 36 | testParseIp = writeScript "test-parse-ip.erl" '' 37 | -module(test_parse_ip). 38 | -export([main/0]). 39 | 40 | -spec main() -> no_return(). 41 | main() -> 42 | Ips = init:get_plain_arguments(), 43 | Result = lists:map(fun(Ip) -> 44 | NixTest = case nix_test(Ip) of 45 | {ok, Result} -> 46 | Stripped = re:replace(Result, "^\"|[\n\"]$", "", 47 | [global, {return, binary}]), 48 | Code = binary_to_list(<>), 49 | case erl_scan:string(Code) of 50 | {ok, Tokens, _} -> 51 | {ok, [Form]} = erl_parse:parse_exprs(Tokens), 52 | {value, Value, _} = erl_eval:expr(Form, []), 53 | {ok, Value}; 54 | {error, _, _} -> {ok, Result} 55 | end; 56 | Rest -> Rest 57 | end, 58 | case {NixTest, inet:parse_strict_address(Ip)} of 59 | {{ok, Res}, {ok, Res}} -> 60 | io:format("Test for IP ~p passed (same value).~n", [Ip]), 61 | true; 62 | {{fail, _}, {error, _}} -> 63 | io:format("Test for IP ~p passed (error on both).~n", [Ip]), 64 | true; 65 | {NixRes, ErlRes} -> 66 | io:format("FAILED test for IP ~p!~n" ++ 67 | " Nix expression returned: ~p~n" ++ 68 | " Erlang returned: ~p~n", [Ip, NixRes, ErlRes]), 69 | false 70 | end 71 | end, Ips), 72 | LazyResult = lists:all(fun(X) -> X end, Result), 73 | erlang:halt(case LazyResult of true -> 0; _ -> 1 end). 74 | 75 | -spec gather_result(port(), binary()) -> {ok | fail, binary()}. 76 | gather_result(Port, Partial) -> 77 | receive 78 | {Port, {data, Data}} -> 79 | gather_result(Port, <>); 80 | {Port, eof} -> Port ! {self(), close}, gather_result(Port, Partial); 81 | {Port, {exit_status, 0}} -> {ok, Partial}; 82 | {Port, {exit_status, _}} -> {fail, Partial}; 83 | _ -> gather_result(Port, Partial) 84 | after 1 -> 85 | gather_result(Port, Partial) 86 | end. 87 | 88 | -spec nix_test(string()) -> {ok | fail, binary()}. 89 | nix_test(Ip) -> 90 | TestLib = "(import { lib = import ; })", 91 | Command = "parseErlIpAddr", 92 | Code = TestLib ++ "." ++ Command ++ " " ++ "\"" ++ Ip ++ "\"", 93 | Args = [ 94 | "-I", "testlib=" ++ "${../lib}", 95 | "-I", "nixpkgslib=" ++ "${}", 96 | "--readonly-mode", "--eval", 97 | "-E", Code 98 | ], 99 | Port = open_port( 100 | {spawn_executable, "${nix}/bin/nix-instantiate"}, 101 | [{args, Args}, binary, in, stream, exit_status, use_stdio, 102 | stderr_to_stdout] 103 | ), 104 | gather_result(Port, <<>>). 105 | ''; 106 | 107 | } '' 108 | export NIX_DB_DIR=$TMPDIR 109 | export NIX_STATE_DIR=$TMPDIR 110 | nix-store --init 111 | 112 | cat "$testParseIp" > test_parse_ip.erl 113 | erlc -Wall test_parse_ip.erl 114 | 115 | erl -smp -noinput -s test_parse_ip main -extra $testAddresses 116 | touch "$out" 117 | ''; 118 | } 119 | -------------------------------------------------------------------------------- /tests/headcounter/listeners.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: let 2 | inherit (import ../mongooseim/lib.nix { 3 | inherit pkgs lib; 4 | }) runInCtl checkListeners; 5 | in { 6 | name = "listeners"; 7 | testScript = { nodes, ... }: 8 | runInCtl "ultron" (checkListeners nodes.ultron); 9 | } 10 | -------------------------------------------------------------------------------- /tests/headcounter/per-vhost/escalus/default.nix: -------------------------------------------------------------------------------- 1 | vhost: 2 | 3 | { pkgs, lib, ... }: let 4 | 5 | testLibs = let 6 | pkg = pkgs.headcounter.mongooseimTests; 7 | deps = lib.singleton pkg ++ pkg.recursiveErlangDeps; 8 | in map (d: "${d.appDir}/ebin") deps; 9 | 10 | users = { 11 | outsider.password = "truly secure, no?"; 12 | outsider.shouldExist = false; 13 | 14 | admin.server = "aszlig.net"; 15 | admin.password = "big admin"; 16 | 17 | wallop.server = "aszlig.net"; 18 | wallop.password = "small wallop"; 19 | 20 | toradmin.server = "torservers.net"; 21 | toradmin.password = "torservers dedicated admin"; 22 | 23 | alice.password = "123456"; 24 | bob.password = "654321"; 25 | }; 26 | 27 | mkTestConfig = hclib: fqdn: realHost: let 28 | mkSpecVal = val: if lib.isString val then { binary = val; } else val; 29 | mkUser = name: spec: { 30 | username.binary = name; 31 | server.binary = spec.server or fqdn; 32 | host.binary = spec.host or realHost; 33 | starttls.atom = "required"; 34 | auth_method.binary = "SCRAM-SHA-1"; 35 | } // lib.mapAttrs (lib.const mkSpecVal) spec; 36 | in pkgs.writeText "test.config" '' 37 | {escalus_xmpp_server, escalus_mongooseim}. 38 | {escalus_host, ${hclib.erlBinary realHost}}. 39 | 40 | {escalus_users, ${hclib.erlPropList (lib.mapAttrs mkUser users)}}. 41 | ''; 42 | 43 | registerUser = fqdn: name: spec: let 44 | user = spec.username or name; 45 | host = spec.host or fqdn; 46 | inherit (spec) password; 47 | shouldExist = spec.shouldExist or true; 48 | cmd = [ "mongooseimctl" "register" name host password ]; 49 | shellCmd = lib.concatMapStringsSep " " lib.escapeShellArg cmd; 50 | perlCmd = "$ultron->succeed('${lib.escape ["'"] shellCmd}');"; 51 | in lib.optionalString shouldExist perlCmd; 52 | 53 | testRunner = fqdn: [ 54 | "${pkgs.erlang}/bin/ct_run" 55 | "-noinput" "-noshell" 56 | "-config" "/etc/headcounter/test.config" 57 | "-ct_hooks" "ct_tty_hook" "[]" 58 | "-env" "FQDN" fqdn 59 | "-logdir" "ct_report" 60 | "-dir" "test" 61 | "-erl_args" 62 | "-pa" 63 | ] ++ testLibs; 64 | 65 | sourceTree = pkgs.runCommand "test-source-tree" {} '' 66 | mkdir -p "$out/test" 67 | cp "${./headcounter_SUITE.erl}" "$out/test/headcounter_SUITE.erl" 68 | ''; 69 | 70 | in { 71 | name = "vhost-${vhost}-escalus"; 72 | 73 | nodes.client = { nodes, hclib, ... }: let 74 | inherit (nodes.ultron.config.headcounter.vhosts.${vhost}) fqdn; 75 | realHost = if fqdn == "torservers.net" then "jabber.${fqdn}" else fqdn; 76 | in { 77 | virtualisation.memorySize = 1024; 78 | environment.etc."headcounter/test.config" = { 79 | source = mkTestConfig hclib fqdn realHost; 80 | }; 81 | }; 82 | 83 | excludeNodes = [ "taalo" "benteflork" "unzervalt" ]; 84 | 85 | testScript = { nodes, ... }: let 86 | inherit (nodes.ultron.config.headcounter.vhosts.${vhost}) fqdn; 87 | in '' 88 | ${lib.concatStrings (lib.mapAttrsToList (registerUser fqdn) users)} 89 | 90 | $client->succeed('cp -Lr ${sourceTree}/* .'); 91 | 92 | ${(import ../../../mongooseim/lib.nix { 93 | inherit pkgs lib; 94 | }).runCommonTests (testRunner fqdn)} 95 | ''; 96 | } 97 | -------------------------------------------------------------------------------- /tests/headcounter/per-vhost/xmppoke.nix: -------------------------------------------------------------------------------- 1 | vhost: 2 | 3 | { 4 | name = "vhost-${vhost}-xmppoke"; 5 | 6 | nodes.client = { pkgs, lib, ... }: let 7 | patchedPoke = lib.overrideDerivation pkgs.headcounter.xmppoke (o: { 8 | postPatch = (o.postPatch or "") + '' 9 | sed -ri -e 's/(db_host *= *)[^,]*/\1nil/' \ 10 | -e '/Connecting to database/d' \ 11 | poke.lua 12 | ''; 13 | }); 14 | in { 15 | services.postgresql = { 16 | enable = true; 17 | package = pkgs.postgresql; 18 | initialScript = pkgs.writeText "init.sql" '' 19 | CREATE ROLE xmppoke WITH LOGIN; 20 | CREATE DATABASE xmppoke OWNER xmppoke; 21 | \c xmppoke 22 | BEGIN; 23 | \i ${patchedPoke}/share/xmppoke/schema.pg.sql 24 | COMMIT; 25 | ''; 26 | authentication = '' 27 | local all xmppoke trust 28 | ''; 29 | }; 30 | 31 | environment.systemPackages = [ 32 | patchedPoke pkgs.headcounter.xmppokeReport pkgs.openssl 33 | ]; 34 | }; 35 | 36 | excludeNodes = [ "taalo" "benteflork" "unzervalt" ]; 37 | 38 | testScript = { nodes, ... }: let 39 | inherit (nodes.ultron.config.headcounter.vhosts.${vhost}) fqdn; 40 | pokeOpts = "--cafile=/etc/ssl/certs/ca-bundle.crt --delay=0"; 41 | 42 | resultSql = "SELECT sr.total_score, sr.grade, tr.type" 43 | + " FROM srv_results sr, test_results tr" 44 | + " WHERE sr.test_id = tr.test_id"; 45 | getResult = "echo '${resultSql}' | psql -t -Pformat=unaligned -F: xmppoke"; 46 | in '' 47 | $client->waitForUnit("network.target"); 48 | 49 | my $srvfqdn; 50 | 51 | $client->nest("check availability", sub { 52 | my $srvreply = $client->succeed("host -t srv _xmpp-server._tcp.${fqdn}"); 53 | $srvreply =~ /has SRV record(?:\s+\S+){3}\s+(\S+)\.$/m; 54 | $srvfqdn = $1; 55 | $client->succeed("ping -c1 $srvfqdn >&2"); 56 | $client->succeed("nc -z $srvfqdn 5222"); 57 | }); 58 | 59 | $client->waitForUnit("postgresql.service"); 60 | 61 | $client->nest("force ACME certificate renewal", sub { 62 | my $newdateUnix = $client->succeed('date +%s --date "80 days"'); 63 | chomp $newdateUnix; 64 | my $newdate = $client->succeed('date +%m%d%H%M%Y --date @'.$newdateUnix); 65 | chomp $newdate; 66 | 67 | $log->nest("fast-forward time to now + 80 days", sub { 68 | $_->execute('date '.$newdate) foreach values %vms; 69 | }); 70 | 71 | $ca->succeed('systemctl restart boulder*'); 72 | 73 | $ultron->execute('systemctl start acme.service'); 74 | 75 | $client->waitUntilSucceeds( 76 | 'echo | openssl s_client -connect '.$srvfqdn.':5223' 77 | ); 78 | 79 | my $issuanceUnix = $client->succeed( 80 | 'fetched="$(echo | openssl s_client -connect '.$srvfqdn.':5223)" && '. 81 | 'parsed="$(echo "$fetched" | openssl x509 -noout -dates)" && '. 82 | 'mangled="$(echo "$parsed" | sed -ne "s/notBefore=//p")" && '. 83 | 'date --date "$mangled" +%s' 84 | ); 85 | 86 | my $issuance = $client->succeed('date --date @'.$issuanceUnix); 87 | chomp $issuance; 88 | 89 | $issuanceUnix > $newdateUnix - 7200 90 | or die "Certificate is too old ($issuance)"; 91 | }); 92 | 93 | $client->nest("xmppoke", sub { 94 | $client->succeed("xmppoke ${pokeOpts} --mode=client '${fqdn}' >&2"); 95 | $client->succeed("xmppoke ${pokeOpts} --mode=server '${fqdn}' >&2"); 96 | 97 | $client->succeed( 98 | "mkdir /tmp/xchg/xmppoke && (cd /tmp/xchg/xmppoke; ". 99 | "xmppoke-genreport client '${fqdn}'; ". 100 | "xmppoke-genreport server '${fqdn}'". 101 | "); sync" 102 | ); 103 | 104 | system("tar xf vm-state-client/xchg/xmppoke/client.tar -C '$out'"); 105 | system("tar xf vm-state-client/xchg/xmppoke/server.tar -C '$out'"); 106 | 107 | open HYDRA, ">>$out/nix-support/hydra-build-products"; 108 | print HYDRA "report tls-c2s $out/xmppoke client.html\n"; 109 | print HYDRA "report tls-s2s $out/xmppoke server.html\n"; 110 | close HYDRA; 111 | 112 | my $failed = 2; 113 | my $result = $client->succeed("${getResult}"); 114 | foreach (split /\n/, $result) { 115 | my @sr = split /:/; 116 | if ($sr[1] eq 'A') { 117 | $failed--; 118 | } else { 119 | $client->log("The $sr[2] test passed with a grade of ". 120 | "$sr[1] instead of A!"); 121 | } 122 | } 123 | if ($failed) { 124 | open TOUCH_FAILED, ">>$out/nix-support/failed"; 125 | close TOUCH_FAILED; 126 | } 127 | }); 128 | ''; 129 | } 130 | -------------------------------------------------------------------------------- /tests/make-test.nix: -------------------------------------------------------------------------------- 1 | testexpr: 2 | 3 | { system ? builtins.currentSystem, ... }@args: let 4 | 5 | inherit (import { 6 | inherit system; 7 | }) makeTest pkgs; 8 | 9 | inherit (pkgs.lib) optionalAttrs mapAttrs; 10 | 11 | testAttrs = if builtins.isFunction testexpr then testexpr (args // { 12 | pkgs = import ../pkgs { inherit pkgs; }; 13 | }) else testexpr; 14 | 15 | nodes = testAttrs.nodes or (optionalAttrs (testAttrs ? machine) { 16 | inherit (testAttrs) machine; 17 | }); 18 | 19 | argsWithCommon = removeAttrs testAttrs [ "machine" ] // { 20 | nodes = mapAttrs (name: config: { 21 | imports = [ ../common.nix config ]; 22 | }) nodes; 23 | }; 24 | 25 | in makeTest argsWithCommon 26 | -------------------------------------------------------------------------------- /tests/mongooseim/lib.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | 3 | { 4 | runInCtl = machine: commands: let 5 | expectScript = pkgs.writeScript "mongooseimctl.expect" '' 6 | #!${pkgs.expect}/bin/expect -f 7 | set timeout 60 8 | 9 | proc sendCommand {command} { 10 | expect -ex {** exception} { 11 | exit 1 12 | } -re {\(mongooseim@${machine}\)\d+>} { 13 | send -- "$command\r" 14 | } timeout { 15 | puts "Erlang shell isn't connected to remote node!" 16 | exit 1 17 | } 18 | } 19 | 20 | set commands [open [lindex $argv 0] r] 21 | spawn mongooseimctl debug 22 | while {-1 != [gets $commands line]} { 23 | sendCommand $line 24 | } 25 | close $commands 26 | sendCommand \007 27 | expect -re {-->} 28 | send "q\r" 29 | exit [lindex [wait] 3] 30 | ''; 31 | cmdFile = pkgs.writeText "ctl-commands.erl" commands; 32 | in "\$${machine}->succeed('${expectScript} ${cmdFile} >&2');"; 33 | 34 | checkListeners = node: let 35 | inherit (node.config.headcounter) services; 36 | inherit (services.mongooseim) configFile; 37 | hclib = import ../../lib { inherit lib; }; 38 | isLocalEpmd = services.epmd.addresses == lib.singleton "127.0.0.1"; 39 | PortAndAddr = "{127, 0, 0, 1, KernelPort}"; 40 | kernelPortOrAddrInfo = if isLocalEpmd then "KernelPort" else PortAndAddr; 41 | in '' 42 | {ok, Config} = file:consult(${hclib.erlString configFile}), 43 | 44 | KernelPid = erlang:process_info(whereis(net_kernel)), 45 | KernelLinks = proplists:get_value(links, KernelPid), 46 | KernelPorts = [inet:port(Port) || Port <- KernelLinks, is_port(Port)], 47 | {ok, KernelPort} = hd(KernelPorts), 48 | 49 | CfgListeners = [element(1, L) || 50 | L <- proplists:get_value(listen, Config, [])], 51 | Listeners = [KernelPort|CfgListeners], 52 | Split = lists:partition(fun({_, _}) -> true; (_) -> false end, Listeners), 53 | {AllowedHostPort, AllowedPorts} = Split, 54 | IsAllowed = fun(Sock) -> 55 | {ok, {Host, Port}} = inet:sockname(Sock), 56 | lists:member({Port, Host}, AllowedHostPort) or 57 | lists:member(Port, AllowedPorts) 58 | end, 59 | IsListening = fun(Port) -> 60 | {ok, Status} = prim_inet:getstatus(Port), 61 | lists:member(listen, Status) 62 | end, 63 | Recurse = fun 64 | (_, Pid, 3) -> Pid; 65 | (Self, Pid, Inc) -> 66 | Info = erlang:process_info(Pid), 67 | Links = proplists:get_value(links, Info, []), 68 | NewLinks = [Self(Self, Link, Inc + 1) || 69 | Link <- Links, is_pid(Link)], 70 | lists:keyreplace(links, 1, Info, {links, NewLinks}) 71 | end, 72 | GetDeepProcessInfo = fun(Pid) -> Recurse(Recurse, Pid, 1) end, 73 | GetSockInfo = fun(Port) -> 74 | {ok, AddrInfo} = inet:sockname(Port), 75 | {links, Links} = erlang:port_info(Port, links), 76 | [[{listen_at, AddrInfo}|GetDeepProcessInfo(Pid)] || Pid <- Links] 77 | end, 78 | Ports = [GetSockInfo(Port) || Port <- erlang:ports(), 79 | erlang:port_info(Port, name) == {name, "tcp_inet"}, 80 | IsListening(Port), not IsAllowed(Port)], 81 | case Ports of 82 | [] -> ok; 83 | _ -> io:format("Stray listeners: ~p~n", [Ports]), 84 | erlang:error(unacceptable) 85 | end. 86 | ''; 87 | 88 | # Returns a shell script fragment for use in testScript during VM tests, which 89 | # runs an Erlang Common Test runner that is passed as a list of command line 90 | # arguments (which should not be escaped already) and it makes sure the VM 91 | # test derivation doesn't fail. 92 | # 93 | # The reason why we don't want it to fail is because we are creating 94 | # $out/nix-support/failed, which causes Hydra to mark the job as failed but we 95 | # still get an output (which in this case is the HTML report of the CT run). 96 | # 97 | # After the runner command has executed the report is expected to be in 98 | # /tmp/ct_report (/tmp is the pwd, so a relative "ct_report" should suffice). 99 | runCommonTests = runnerCmdline: let 100 | runner = lib.concatMapStringsSep " "lib.escapeShellArg runnerCmdline; 101 | in '' 102 | my $testCmd = 'mkdir -p ct_report && ${lib.escape ["'"] runner} >&2'; 103 | 104 | $client->nest("running test suite: $testCmd", sub { 105 | my $rval = ($client->execute_($testCmd))[0]; 106 | my $out = $ENV{'out'}; 107 | 108 | my $rawugly = $client->succeed( 109 | 'find ct_report -name \'*@*\' -print | '. 110 | 'xargs -I{} sh -c \'mv "{}" "$(echo "{}" | '. 111 | 'tr @ _)" && basename "{}"\' ' 112 | ); 113 | chomp $rawugly; 114 | my @uglynames = split "\n", $rawugly; 115 | foreach my $ugly (@uglynames) { 116 | $client->succeed('find ct_report -type f -exec '. 117 | "sed -i -e 's|$ugly|".($ugly =~ s/\@/_/gr)."|' {} +"); 118 | } 119 | 120 | $client->succeed('tar cf /tmp/xchg/ct_report.tar ct_report && sync'); 121 | system("tar xf vm-state-client/xchg/ct_report.tar -C '$out'"); 122 | 123 | open HYDRA_PRODUCTS, ">>$out/nix-support/hydra-build-products"; 124 | print HYDRA_PRODUCTS "report ct-tests $out/ct_report\n"; 125 | close HYDRA_PRODUCTS; 126 | 127 | my @summaries = <$out/ct_report/ct_run.*/*.logs/run.*/suite.summary>; 128 | my @stats; 129 | foreach my $stat (@summaries) { 130 | open STAT, $stat; 131 | my @row = split(/\D+/, ); 132 | $stats[$_] += $row[$_ + 1] for (0 .. ($#row - 1)); 133 | close STAT 134 | } 135 | 136 | my $total = $stats[0] + $stats[1]; 137 | my $skip = $stats[2] + $stats[3]; 138 | $client->log("$stats[0] out of $total tests succeeded ($skip skipped)"); 139 | 140 | if ($stats[1] > 0 || $stats[0] < $total) { 141 | $client->log("$stats[1] tests failed."); 142 | open TOUCH_FAILED, ">>$out/nix-support/failed"; 143 | close TOUCH_FAILED; 144 | } 145 | }); 146 | ''; 147 | } 148 | -------------------------------------------------------------------------------- /tests/nsd-zone-writer.nix: -------------------------------------------------------------------------------- 1 | import ./make-test.nix ({ lib, ... }: { 2 | name = "nsd-zone-writer"; 3 | 4 | machine = { pkgs, ... }: { 5 | services.nsd = { 6 | enable = true; 7 | interfaces = lib.mkForce []; 8 | zones."dummy.".data = "@ SOA ns.dummy noc.dummy ( 1 1h 1h 1h 1h )\n"; 9 | }; 10 | environment.systemPackages = [ pkgs.bind.dnsutils ]; 11 | headcounter.nsd-zone-writer = { 12 | enable = true; 13 | allowedUsers = [ "alice" ]; 14 | }; 15 | users.users = { 16 | alice.isNormalUser = true; 17 | bob.isNormalUser = true; 18 | }; 19 | }; 20 | 21 | testScript = { nodes, ... }: let 22 | writeZone = nodes.machine.config.headcounter.nsd-zone-writer.command; 23 | writeTestZone = cmd: user: fqdn: let 24 | testZone = '' 25 | @ SOA ns.${fqdn}. noc.${fqdn}. ( 1 3h 1h 1w 1d ) 26 | test IN A 1.2.3.4 27 | ''; 28 | userCmd = "echo ${lib.escapeShellArg testZone} | ${writeZone} ${fqdn}"; 29 | shellCmd = "su -c ${lib.escapeShellArg userCmd} ${user}"; 30 | in "\$machine->${cmd}('${lib.escape ["'" "\\"] shellCmd}');"; 31 | in '' 32 | sub checkZone ($) { 33 | my ($fqdn) = @_; 34 | my $out = $machine->succeed( 35 | 'dig +noall +answer @127.0.0.1 test.'.$fqdn.' a' 36 | ); 37 | chomp $out; 38 | $out =~ /^test\.\Q$fqdn.\E\s+\d+\s+IN\s+A\s+\Q1.2.3.4\E$/ 39 | or die "Zone for $fqdn not available (output: '$out')!"; 40 | } 41 | 42 | sub checkStat ($$$$) { 43 | my ($path, $modes, $owner, $group) = @_; 44 | my $out = $machine->succeed('stat -c %A:%U:%G '.$path); 45 | chomp $out; 46 | my ($oModes, $oOwner, $oGroup) = split /:/, $out; 47 | my $onPath = " on path $path "; 48 | $modes eq $oModes or die "Expected modes$onPath$modes but got $oModes!"; 49 | $owner eq $oOwner or die "Expected owner$onPath$owner but got $oOwner!"; 50 | $group eq $oGroup or die "Expected owner$onPath$group but got $oGroup!"; 51 | } 52 | 53 | $machine->waitForUnit('multi-user.target'); 54 | checkStat "/var/lib/nsd", "drwx--x---", "nsd", "dynzone"; 55 | checkStat "/var/lib/nsd/dynzones", "drwx--s---", "dynzone", "nsd"; 56 | ${writeTestZone "succeed" "alice" "foo"} 57 | checkZone "foo"; 58 | checkStat "/var/lib/nsd/dynzones/foo.zone", "-rw-r-----", "dynzone", "nsd"; 59 | ${writeTestZone "fail" "bob" "bar"} 60 | $machine->fail('stat /var/lib/nsd/dynzones/bar.zone'); 61 | 62 | $machine->shutdown; 63 | $machine->waitForUnit('multi-user.target'); 64 | ${writeTestZone "succeed" "alice" "xxx"} 65 | checkZone "xxx"; 66 | ''; 67 | }) 68 | -------------------------------------------------------------------------------- /tests/postfix.nix: -------------------------------------------------------------------------------- 1 | import ./make-test.nix { 2 | name = "postfix"; 3 | 4 | nodes = { 5 | server = { 6 | headcounter.services.postfix.enable = true; 7 | }; 8 | client = { pkgs, ... }: { 9 | environment.systemPackages = [ pkgs.swaks ]; 10 | }; 11 | }; 12 | 13 | testScript = '' 14 | startAll; 15 | 16 | $server->waitForOpenPort(25); 17 | $client->succeed('swaks --to root@server'); 18 | ''; 19 | } 20 | -------------------------------------------------------------------------------- /tests/postgresql.nix: -------------------------------------------------------------------------------- 1 | import ./make-test.nix ({ lib, ... }: { 2 | name = "postgresql"; 3 | 4 | machine = { config, pkgs, lib, ... }: { 5 | services.postgresql.enable = true; 6 | 7 | users.users.tester.isNormalUser = true; 8 | users.users.alice.isNormalUser = true; 9 | users.users.bob.isNormalUser = true; 10 | 11 | headcounter.postgresql.databases = let 12 | schemaFile = pkgs.writeText "create-lookup.sql" '' 13 | CREATE TABLE lookup (id SERIAL); 14 | ''; 15 | in { 16 | foo = { 17 | extraSql = '' 18 | CREATE SCHEMA private; 19 | CREATE SEQUENCE private.fooseq; 20 | CREATE TABLE private.bar (id SERIAL); 21 | CREATE TABLE foo (value TEXT); 22 | ''; 23 | 24 | inherit schemaFile; 25 | 26 | users.alice.permissions = lib.mkOptionDefault { 27 | schema.private.all = true; 28 | allSequences.private.all = true; 29 | allTables.private.all = true; 30 | }; 31 | 32 | users.bob.permissions = lib.mkForce { 33 | table.public.foo.select = true; 34 | table.private.bar.insert = true; 35 | sequence.private.fooseq.select = true; 36 | schema.public.usage = true; 37 | schema.private.usage = true; 38 | }; 39 | 40 | users.tester.permissions = lib.mkForce { 41 | schema.public.usage = true; 42 | table.public.foo.insert = true; 43 | }; 44 | 45 | neededBy = [ "testdb.service" ]; 46 | }; 47 | 48 | bar = { 49 | extraSql = '' 50 | CREATE SCHEMA secret; 51 | CREATE TABLE secret.testme (value TEXT); 52 | INSERT INTO secret.testme VALUES('magic'); 53 | ''; 54 | 55 | inherit schemaFile; 56 | 57 | users.tester.permissions = lib.mkForce { 58 | table.secret.testme.select = true; 59 | schema.secret.usage = true; 60 | }; 61 | 62 | neededBy = [ "testdb.service" ]; 63 | }; 64 | }; 65 | 66 | systemd.services.testdb = { 67 | description = "Test PostgreSQL Database `bar'"; 68 | wantedBy = [ "multi-user.target" ]; 69 | serviceConfig.RemainAfterExit = true; 70 | serviceConfig.User = "tester"; 71 | path = [ config.services.postgresql.package ]; 72 | script = '' 73 | value="$(psql -tAq -c 'SELECT * FROM secret.testme' bar)" 74 | psql -c "INSERT INTO foo VALUES('$value');" foo 75 | ''; 76 | }; 77 | }; 78 | 79 | testScript = '' 80 | ${lib.concatMapStrings (what: '' 81 | sub sql${what} ($$$) { 82 | my ($user, $db, $sql) = @_; 83 | $sql =~ s/'/'\\'''/g; 84 | my $cmd = "psql -c '$sql' $db"; 85 | $cmd =~ s/'/'\\'''/g; 86 | return $machine->${lib.toLower what}("su -c '$cmd' $user"); 87 | } 88 | '') [ "Succeed" "Fail" ]} 89 | 90 | $machine->waitForUnit('testdb.service'); 91 | $machine->waitForUnit('multi-user.target'); 92 | 93 | sqlSucceed "alice", "foo", "SELECT setval('private.fooseq', 1)"; 94 | 95 | sqlFail "bob", "foo", "INSERT INTO foo VALUES('123')"; 96 | 97 | sqlSucceed("bob", "foo", "SELECT * FROM foo") =~ /magic/ 98 | or die "No magic found in table foo.public.foo."; 99 | 100 | sqlFail "bob", "foo", "SELECT * FROM private.bar"; 101 | sqlSucceed "bob", "foo", "INSERT INTO private.bar VALUES(321)"; 102 | sqlSucceed "bob", "foo", "SELECT last_value FROM private.fooseq"; 103 | sqlFail "bob", "foo", "SELECT nextval('private.fooseq')"; 104 | 105 | sqlFail "bob", "bar", "SELECT * FROM lookup"; 106 | 107 | sqlSucceed "tester", "bar", "SELECT * FROM secret.testme"; 108 | sqlFail "tester", "foo", "SELECT * FROM foo"; 109 | 110 | sqlSucceed "alice", "foo", "SELECT * FROM lookup"; 111 | sqlFail "alice", "bar", "SELECT * FROM lookup"; 112 | 113 | sqlSucceed "alice", "foo", "INSERT INTO foo VALUES('abc')"; 114 | sqlSucceed "alice", "foo", "INSERT INTO private.bar VALUES(456)"; 115 | sqlSucceed "alice", "foo", "SELECT last_value FROM private.fooseq"; 116 | sqlFail "alice", "foo", "CREATE TABLE xyz (id SERIAL)"; 117 | 118 | sqlSucceed "alice", "foo", "CREATE TABLE private.uvw (id SERIAL)"; 119 | ''; 120 | }) 121 | --------------------------------------------------------------------------------