diff --git a/profiles/helix-zone/configuration.nix b/profiles/helix-zone/configuration.nix new file mode 100644 index 00000000..c64d62bd --- /dev/null +++ b/profiles/helix-zone/configuration.nix @@ -0,0 +1,60 @@ +{ config, pkgs, ... }: +{ + imports = + [ # Include the results of the hardware scan. + ./hardware-configuration.nix + ./wireguard.nix + ./matrix.nix + ./monitoring.nix + ./rss-bridge.nix + ./gitea.nix + # ./dns.nix + "${builtins.fetchTarball "https://github.com/ryantm/agenix/archive/main.tar.gz"}/modules/age.nix" + ]; + + # Use the GRUB 2 boot loader. + boot.loader.grub.enable = true; + boot.loader.grub.version = 2; + boot.loader.grub.device = "/dev/vda"; # or "nodev" for efi only + + networking.hostName = "helix"; # Define your hostname. + + networking.useDHCP = false; + networking.interfaces.ens3.useDHCP = true; + + # Configure network proxy if necessary + # networking.proxy.default = "http://user:password@proxy:port/"; + # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; + + # Enable the OpenSSH daemon. + services.openssh.enable = true; + services.openssh.passwordAuthentication = false; + services.openssh.permitRootLogin = "no"; + + # Open ports in the firewall. + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + # networking.firewall.enable = false; + + # Include agenix + #environment.systemPackages = [ agenix.defaultPackage.x86_64-linux ]; + + # Define a user account. Don't forget to set a password with ‘passwd’. + users.users.anish = { + isNormalUser = true; + extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user + openssh.authorizedKeys.keys = [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM0Zvei46x/yZl/IeBCq6+IYQQ0avulzVyBysF9cPigZMCybWRV7IEU+E3k9t6JrbdbdGfJkcZIWmsWDdKS8W8mBnZpVoT0ffLynu8JQ/TKdGm4Qv6bgUeKNrGsNv0ZPs2CDaGSLj0oJfRF7Ko10tcLP0vW+yujrh+y6TH/vVzJioaV4TGvtCUpn+wEQah9ROwPQLUUofsSWdnRsDJ/gp37zXWs4l5wyjSKtP3O9RZUP7kBekbSqEgSXiTk0oUQSVqIWl9NDiP6onk/gSOjXsR/JPqsSN/XI/c/yj6gyY0f51Ru2D7iBxuMJIJcWV+rU6coIj+ULcQWLzt/7TI8jq5AOOzI/ll4zbL24Eo84Rz+TP9tvMMhDZ0VaMN22AJ8qQEjc5P09tWKsX7Jg39XelyV1jHXncE4yvIE9F4RSCHzWCeKeXakizQNuzSaxTxIExRFYHjNW5bR6+3MTGwVrEIXU+qML+0yFTR86MT+tdY5AreAJQLwbog79O1NupeXJE= anish@curve" + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCeWQsf9rbE+KBrWLkLkg1PMKx0jPfDP8xyv+XtddIQzAskRTN+JSSQp1GpoRgfQij7bx6f6DfjkwSQK5NWDQNQ+klGhcdEwEdptWUzpRAvKHMImjyIwFHmRKKEgcfH8+DSHVM93nRSQKrLezX4PzPkHF9f3TLxFdGeQDeY0J2oP2Oa97hRtuTJZW9BwUojyvDYUbg7F3fxf+pdFCvIAFaayiLkhOYdTXnjLt1Ks6NOhSzM78ZibHv4pFYI9r6dhflkFP/SzyLeSSm1aVAtxqE7vF5HnnORAg0byDsiOv/9RXxiDx7gkAAw3xENDSpp9TRj54M7iz1OSsRMhmD6orHjZFIRejlE4DIn+PBPm3PH+U5ILKTogE6n5MfPT28V0QdtHuqq3b5tSWiDcFpnjxbIUmEIo5hLzMR0tXZoNLQhBCSNfxjE8CYzpk7mHbvhSs2GPfG+NI1vvldbw2EF8beJFQHNYTqJw8y7p8RXpCwZqqpSitJX6v9DiYdLVG1hWvE= nix-on-droid@localhost" + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKN8/SH55DBiwVoSnTU8k2Pen+wmovL9QaMyehxGEsJJv/8fzwsswGalD4C/4O51LOvdu4UKkZW5hG02uVSK+58p3UV3pOPyoqsu/aDeIsWsqmTeTzUrBIWOlNzcDKnohLz2oGC5YO+wyTJ9Iteq6aGJDjErsW7sG3h5lXCs551EmJNNGhtBQaaoytMNnWqSdlVjDNCijurH7WUpp40U/RjEp532l4rX6eIIj3jBKEFbhZkFSSjqbj4xM4SyFt+Jmigb1RMjsQjmpfY1vDtM84RcYfpTUte/T5w2dkD5H6kccmWnwKSJpm9wXfx4E7lR9APdUGnau2U1+XxiD3ytGl anish@box" ]; + }; + + # This value determines the NixOS release with which your system is to be + # compatible, in order to avoid breaking some software such as database + # servers. You should change this only after NixOS release notes say you + # should. + system.stateVersion = "19.09"; # Did you read the comment? + +} + diff --git a/profiles/helix-zone/dns.nix b/profiles/helix-zone/dns.nix new file mode 100644 index 00000000..a23adc88 --- /dev/null +++ b/profiles/helix-zone/dns.nix @@ -0,0 +1,54 @@ +{ pkgs, ... }: + +let + adblockLocalZones = pkgs.stdenv.mkDerivation { + name = "unbound-zones-adblock"; + + src = (pkgs.fetchFromGitHub { + owner = "StevenBlack"; + repo = "hosts"; + rev = "3.0.0"; + sha256 = "01g6pc9s1ah2w1cbf6bvi424762hkbpbgja9585a0w99cq0n6bxv"; + } + "/hosts"); + + phases = [ "installPhase" ]; + + installPhase = '' + ${pkgs.gawk}/bin/awk '{sub(/\r$/,"")} {sub(/^127\.0\.0\.1/,"0.0.0.0")} BEGIN { OFS = "" } NF == 2 && $1 == "0.0.0.0" { print "local-zone: \"", $2, "\" static"}' $src | tr '[:upper:]' '[:lower:]' | sort -u > $out + ''; + }; + +in { + networking.firewall.allowedTCPPorts = [ 53 853 5353 ]; + networking.firewall.allowedUDPPorts = [ 53 853 5353 ]; + networking.extraHosts = '' 127.0.0.1 helix.domain ''; + # These are causing errors, goes in the server file + # local-data = "mossnet.lan. 10800 IN A 10.0.69.4"; + services.unbound = { + enable = true; + settings = { + server = { + interface = [ "0.0.0.0" ]; + tls-upstream = true; + tls-cert-bundle = "/etc/ssl/certs/ca-certificates.crt"; + include = "${adblockLocalZones}"; + access-control = "0.0.0.0/0 allow"; + verbosity = 2; + do-tcp = true; + do-ip4 = true; + prefetch = true; + prefetch-key = true; + rrset-roundrobin = true; + qname-minimisation-strict = true; + hide-identity = true; + hide-version = true; + local-zone = "mossnet.lan. static"; + }; + forward-zone = [{ + name = "."; + forward-addr = [ "45.90.28.77#c46fd3.dns1.nextdns.io" "2a07:a8c0::#c46fd3.dns1.nextdns.io" "45.90.30.77#c46fd3.dns2.nextdns.io" "2a07:a8c1::#c46fd3.dns2.nextdns.io" ]; + }]; + remote-control.control-enable = true; + }; + }; +} diff --git a/profiles/helix-zone/gitea.nix b/profiles/helix-zone/gitea.nix new file mode 100644 index 00000000..fe3488da --- /dev/null +++ b/profiles/helix-zone/gitea.nix @@ -0,0 +1,53 @@ +{ config, lib, pkgs, ... }: + +{ + services.gitea = { + enable = true; + appName = "Sealight Git Forge"; + domain = "git.sealight.xyz"; + rootUrl = "https://git.sealight.xyz"; + httpPort = 3001; + database = { + type = "postgres"; + # passwordFile = "/run/secrets/gitea-dbpass"; # supplied by agenix + password = "somethingunknowablesorry"; + }; + settings = { + metrics = { + ENABLED = true; + }; + repository = { + DEFAULT_BRANCH = "main"; + }; + }; + }; + + + services.postgresql = { + enable = true; # Ensure postgresql is enabled + authentication = '' + local gitea all ident map=gitea-users + ''; + identMap = # Map the gitea user to postgresql + '' + gitea-users gitea gitea + ''; + # ensureDatabases = [ "gitea" ]; + # ensureUsers = [ + # { name = "gitea"; ensurePermissions."DATABASE gitea" = "ALLPRIVLEGES"; } + # ]; + }; + + services.nginx = { + enable = true; # Enable Nginx + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + virtualHosts."git.sealight.xyz" = { # Gitea hostname + enableACME = true; # Use ACME certs + forceSSL = true; # Force SSL + locations."/".proxyPass = "http://localhost:3001/"; # Proxy Gitea + }; + }; +} diff --git a/profiles/helix-zone/hardware-configuration.nix b/profiles/helix-zone/hardware-configuration.nix new file mode 100644 index 00000000..a80788fe --- /dev/null +++ b/profiles/helix-zone/hardware-configuration.nix @@ -0,0 +1,24 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, ... }: + +{ + imports = + [ + ]; + + boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "sr_mod" "virtio_blk" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/3b8b2c4b-835e-4822-8ac8-0effdc8270d6"; + fsType = "btrfs"; + }; + + swapDevices = [ ]; + + nix.maxJobs = lib.mkDefault 1; +} diff --git a/profiles/helix-zone/knot.nix b/profiles/helix-zone/knot.nix new file mode 100644 index 00000000..d6f11947 --- /dev/null +++ b/profiles/helix-zone/knot.nix @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: Apache-2.0 +{ config, pkgs, ... }: +let + # https://github.com/StevenBlack/hosts/issues/451 + # https://github.com/ScriptTiger/Hosts-Conversions + # https://github.com/ScriptTiger/scripttiger.github.io + # https://scripttiger.github.io/alts/ + adblockLocalZones = pkgs.stdenv.mkDerivation { + name = "adblock-rpz"; + src = (pkgs.fetchFromGitHub { + owner = "ScriptTiger"; + repo = "scripttiger.github.io"; + # nix-prefetch-github ScriptTiger scripttiger.github.io + rev = "04402a6726f97c5f0d30436a70ac1344dccb7cf1"; + sha256 = "iSTR7j7QEr5xYtImyntDlVLbnN2ipcLcTRr4sfdx078="; + } + "/alts/rpz/blacklist.txt"); + phases = [ "installPhase" ]; + installPhase = "install -m 444 -D $src $out"; + }; + domain = "sealight.xyz"; + certdir = config.security.acme.certs.${domain}.directory; +in { + networking.firewall.allowedTCPPorts = [ 853 ]; + networking.firewall.allowedUDPPorts = [ 853 ]; + + # for acme certs, TODO: make a proper group + users.users.knot-resolver.extraGroups = [ "nginx" ]; + + # https://github.com/NixOS/nixpkgs/issues/81109 + nixpkgs.config.packageOverrides = pkgs: rec { + knot-resolver = pkgs.knot-resolver.override { extraFeatures = true; }; + }; + services.kresd = { + enable = true; + # Plain DNS only from localhost. + # You might want to add a LAN or VPN subnet, depending on deployment. + #listenPlain = [ "127.0.0.1:53" "[::1]:53" ]; + listenTLS = [ "853" ]; + #listenDoH = [ "" "[::]:443" ]; + instances = 1; + # TODO: chain.pem for stapling: https://gitlab.nic.cz/knot/knot-resolver/-/issues/517 + #policy.add( + # policy.rpz( + # policy.DENY_MSG('domain blocked by your resolver operator'), + # '${adblockLocalZones}', + # false)) + extraConfig = '' + modules.load('predict') + modules = { 'hints > iterate' } + policy.add( + policy.all( + policy.TLS_FORWARD({ + {'45.90.30.0', hostname='87b4e9.dns2.nextdns.io'}, + {'45.90.28.0', hostname='87b4e9.dns1.nextdns.io'} + }))) + policy.add(policy.suffix(policy.FORWARD({'10.0.69.4', '172.16.11.240'}), {todname('mossnet.lan')})) + + net.tls("${certdir}/fullchain.pem", "${certdir}/key.pem") + + -- refresh certs when ACME updates them + -- TODO: could probably do with systemd and a control socket? + -- TODO: may fail on first boot if ACME isn't fast, add a systemd dep + local notify = require('cqueues.notify') + local watcher = notify.opendir('${certdir}') + watcher:add('fullchain.pem') + + worker.coroutine(function () + for flags, name in watcher:changes() do + for flag in notify.flags(flags) do + print('file watcher:', name, notify[flag]) + end + if name == 'fullchain.pem' then + net.tls("${certdir}/fullchain.pem", "${certdir}/key.pem") + end + end + end) + + -- Below we create a tmpfs FS for the cache -- use almost all of it: + cache.size = cache.fssize() - 1*MB + ''; + }; +} diff --git a/profiles/helix-zone/matrix-slack.nix b/profiles/helix-zone/matrix-slack.nix new file mode 100644 index 00000000..b3b071c2 --- /dev/null +++ b/profiles/helix-zone/matrix-slack.nix @@ -0,0 +1,153 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + dataDir = "/var/lib/matrix-appservice-slack"; + registrationFile = "${dataDir}/slack-registration.yaml"; + #appDir = "${pkgs.matrix-appservice-slack}/${pkgs.matrix-appservice-slack.passthru.nodeAppDir}"; + cfg = config.services.matrix-appservice-slack; + # TODO: switch to configGen.json once RFC42 is implemented + settingsFile = pkgs.writeText "matrix-appservice-slack-settings.json" (builtins.toJSON cfg.settings); + +in { + options = { + services.matrix-appservice-slack = { + enable = mkEnableOption "a bridge between Matrix and Slack"; + + settings = mkOption rec { + # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented + type = types.attrs; + apply = recursiveUpdate default; + default = { + # empty values necessary for registration file generation + # actual values defined in environmentFile + auth = { + clientID = ""; + botToken = ""; + }; + }; + example = literalExample '' + { + bridge = { + domain = "public-domain.tld"; + homeserverUrl = "http://public-domain.tld:8008"; + }; + } + ''; + description = '' + config.yaml configuration as a Nix attribute set. + + + + Configuration options should match those described in + + config.sample.yaml. + + + + and + should be set to match the public host name of the Matrix homeserver for webhooks and avatars to work. + + + + Secret tokens should be specified using + instead of this world-readable attribute set. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + File containing environment variables to be passed to the matrix-appservice-discord service, + in which secret tokens can be specified securely by defining values for + APPSERVICE_SLACK_AUTH_CLIENT_I_D and + APPSERVICE_SLACK_AUTH_BOT_TOKEN. + ''; + }; + + url = mkOption { + type = types.str; + default = "http://localhost:${toString cfg.port}"; + description = '' + The URL where the application service is listening for HS requests. + ''; + }; + + port = mkOption { + type = types.port; + default = 9898; # from https://github.com/matrix-org/matrix-appservice-slack/blob/develop/config/config.sample.yaml#L70 + description = '' + Port number on which the bridge should listen for internal communication with the Matrix homeserver. + ''; + }; + + localpart = mkOption { + type = with types; nullOr str; + default = null; + description = '' + The user_id localpart to assign to the AS. + ''; + }; + + serviceDependencies = mkOption { + type = with types; listOf str; + default = optional config.services.matrix-synapse.enable "matrix-synapse.service"; + description = '' + List of Systemd services to require and wait for when starting the application service, + such as the Matrix homeserver if it's running on the same host. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.matrix-appservice-slack = { + description = "A bridge between Matrix and Slack."; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + + preStart = '' + if [ ! -f '${registrationFile}' ]; then + ${pkgs.matrix-appservice-slack}/bin/matrix-appservice-slack \ + --generate-registration \ + --url=${escapeShellArg cfg.url} \ + ${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \ + --config='${settingsFile}' \ + --file='${registrationFile}' + fi + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + PrivateTmp = true; + #WorkingDirectory = appDir; + StateDirectory = baseNameOf dataDir; + UMask = 0027; + EnvironmentFile = cfg.environmentFile; + + ExecStart = '' + ${pkgs.matrix-appservice-slack}/bin/matrix-appservice-slack \ + --file='${registrationFile}' \ + --config='${settingsFile}' \ + --port='${toString cfg.port}' + ''; + }; + }; + }; + + meta.maintainers = with maintainers; [ chickensoupandrice ]; +} + diff --git a/profiles/helix-zone/matrix.nix b/profiles/helix-zone/matrix.nix new file mode 100644 index 00000000..4d75f17d --- /dev/null +++ b/profiles/helix-zone/matrix.nix @@ -0,0 +1,375 @@ +{ config, lib, pkgs, ... }: + +{ + imports = [ + #./mod-mx-puppet-slack.nix + ./mautrix-whatsapp.nix + ./mod-heisenbridge.nix + ]; + security.acme.email = "anish+acme@lakhwara.com"; + security.acme.acceptTerms = true; + services.matrix-synapse = { + enable = true; + app_service_config_files = [ + # # The registration file is automatically generated after starting the appservice for the first time. + # # cp /var/lib/matrix-appservice-discord/discord-registration.yaml /var/lib/matrix-synapse/ + # # chown matrix-synapse:matrix-synapse /var/lib/matrix-synapse/discord-registration.yaml + "/var/lib/matrix-synapse/discord-registration.yaml" + "/var/lib/matrix-synapse/telegram-registration.yaml" + #"/var/lib/matrix-synapse/slack-registration.yaml" + #"/var/lib/matrix-synapse/whatsapp-registration.yaml" + ]; + server_name = "sealight.xyz"; + registration_shared_secret = "hD9HQGTTDxp0mQsQ5JDsfudWMDiubmZENOgPchIvfBvUlPxlvQSvjoO4wn2L1seU"; + enable_metrics = true; + enable_registration = true; + database_type = "psycopg2"; + database_args = { + database = "matrix-synapse"; + }; + + listeners = [ + { + port = 8448; + tls = false; + resources = [{ + compress = true; + names = ["client" "federation"]; + }]; + } + { + port = 9090; + type = "metrics"; + bind_address = "0.0.0.0"; + resources = [{ + compress = false; + names = []; + }]; + tls = false; + } + ]; + extraConfig = '' + max_upload_size: "50M" + use_presence: false + ''; + ## coturn based TURN server integration (TURN server setup mentioned later), + ## shared secret generated while configuring coturn + ## and reused here (power of Nix being a real programming language) + turn_uris = [ + "turn:turn.sealight.xyz:3478?transport=udp" + "turn:turn.sealight.xyz:3478?transport=tcp" + ]; + turn_shared_secret = config.services.coturn.static-auth-secret; + }; + + + services.coturn = { + enable = true; + use-auth-secret = true; + static-auth-secret = "jXW1ohIq6wM3NB00xeME3uBihY85xjpkhGoyzBIdwhOpj7gjyxXZu1fwp1lUiYwJ"; + realm = "turn.sealight.xyz"; + min-port = 49111; + max-port = 51111; + no-cli = true; + no-tcp-relay = true; + no-tls = true; + cert = "${config.security.acme.certs."turn.sealight.xyz".directory}/full.pem"; + pkey = "${config.security.acme.certs."turn.sealight.xyz".directory}/key.pem"; + extraConfig = '' + verbose + user-quota=12 + total-quota=1200 + denied-peer-ip=10.0.0.0-10.255.255.255 + denied-peer-ip=192.168.0.0-192.168.255.255 + denied-peer-ip=172.16.0.0-172.31.255.255 + denied-peer-ip=0.0.0.0-0.255.255.255 + denied-peer-ip=100.64.0.0-100.127.255.255 + denied-peer-ip=127.0.0.0-127.255.255.255 + denied-peer-ip=169.254.0.0-169.254.255.255 + denied-peer-ip=192.0.0.0-192.0.0.255 + denied-peer-ip=192.0.2.0-192.0.2.255 + denied-peer-ip=192.88.99.0-192.88.99.255 + denied-peer-ip=198.18.0.0-198.19.255.255 + denied-peer-ip=198.51.100.0-198.51.100.255 + denied-peer-ip=203.0.113.0-203.0.113.255 + denied-peer-ip=240.0.0.0-255.255.255.255 + ''; + }; + + security.acme.certs.${config.services.coturn.realm} = { + /* insert here the right configuration to obtain a certificate */ + webroot = "/var/lib/acme/acme-challenge/"; + email = "anish+acme@lakhwara.com"; + postRun = "systemctl restart coturn.service"; + group = "turnserver"; + }; + + services.jitsi-meet = { + enable = true; + hostName = "jitsi.sealight.xyz"; + }; + services.jitsi-videobridge.enable = true; + + services.mautrix-telegram = { + enable = true; + environmentFile = /etc/secrets/telegram.env; # file containing the appservice and telegram tokens + # The appservice is pre-configured to use SQLite by default. It's also possible to use PostgreSQL. + settings = { + homeserver = { + address = "https://sealight.xyz"; + domain = "sealight.xyz"; + }; + appservice = { + provisioning.enabled = false; + id = "telegram"; + bot_username = "telegrambridge"; + public = { + enabled = false; + prefix = "/public"; + external = "https://chat.sealight.xyz/public"; + }; + address = "http://localhost:18787"; + port = 18787; + # The service uses SQLite by default, but it's also possible to use PostgreSQL instead: + #database = "postgresql:///mautrix-telegram?host=/run/postgresql"; + }; + bridge = { + relaybot.authless_portals = false; + permissions = { + "@aynish:sealight.xyz" = "admin"; + }; + }; + }; + }; + + services.mautrix-whatsapp = { + enable = false; + # The appservice is pre-configured to use SQLite by default. It's also possible to use PostgreSQL. + #configOptions = { + # homeserver = { + # address = "https://chat.sealight.xyz"; + # domain = "sealight.xyz"; + # }; + # appservice = { + # id = "whatsapp"; + # address = http://localhost:9897; + # hostname = "0.0.0.0"; + # port = 9897; + # database = { + # type = "sqlite3"; + # uri = "/var/lib/mautrix-whatsapp/mautrix-whatsapp.db"; + # }; + # state_store_path = "/var/lib/mautrix-whatsapp/mx-state.json"; + # bot = { + # username = "whatsappbot"; + # displayname = "WhatsApp bridge bot"; + # avatar = "mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr"; + # }; + # as_token = ""; + # hs_token = ""; + # }; + # bridge = { + # username_template = "whatsapp_{{.}}"; + # displayname_template = "{{if .Notify}}{{.Notify}}{{else}}{{.Jid}}{{end}} (WA)"; + # command_prefix = "!wa"; + # permissions = { + # "@aynish:sealight.xyz" = 100; + # }; + # }; + # logging = { + # directory = "/var/lib/mautrix-whatsapp/logs"; + # file_name_format = "{{.Date}}-{{.Index}}.log"; + # file_date_format = "\"2006-01-02\""; + # file_mode = 384; + # timestamp_format = "Jan _2, 2006 15:04:05"; + # print_level = "debug"; + # }; + # metrics = { + # enabled = true; + # listen = "http://localhost:5070"; + # }; + #}; + }; + + services.mx-puppet-discord = { + enable = true; + settings = { + bridge = { + bindAddress = "localhost"; + port = 16785; + domain = "sealight.xyz"; + homeserverUrl = "https://sealight.xyz"; + avatarUrl = "https://discord.com/assets/2d20a45d79110dc5bf947137e9d99b66.svg"; + }; + provisioning = { + whitelist = [ "@aynish:sealight.xyz" ]; + }; + relay = { + whitelist = [ "@aynish:sealight.xyz" ]; + }; + namePatterns = { + group = ":name"; + room = ":name"; + user = ":name (Discord)"; + userOverride = ":displayname"; + }; + presence = { + enabled = false; + }; + }; + serviceDependencies = ["matrix-synapse.service"]; + }; + + #services.mx-puppet-slack= { + #enable = false; + #settings = { + # bridge = { + # bindAddress = "localhost"; + # port = 16786; + # domain = "sealight.xyz"; + # homeserverUrl = "https://sealight.xyz"; + # }; + # provisioning = { + # whitelist = [ "@aynish:sealight.xyz" ]; + # }; + # relay = { + # whitelist = [ "@aynish:sealight.xyz" ]; + # }; + # namePatterns = { + # group = ":name"; + # room = ":name[:team? - :team,]"; + # user = ":name (Slack)"; + # userOverride = ":displayname"; + # }; + # presence = { + # enabled = false; + # }; + # oauth = { + # enabled = false; + # }; + #}; + #serviceDependencies = ["matrix-synapse.service"]; + #}; + + services.heisenbridge = { + enable = true; + homeserver = "https://sealight.xyz"; + listenPort = 14456; + appServiceToken = "wyujLh8kjpmk2bfKeEE3sZ2gWOEUBKK5"; + homeserverToken = "yEHs7lthD2ZHUibJOAv1APaFhEjxN5PT"; + }; + + services.postgresql = { + enable = true; + ## postgresql user and db name remains in the + ## service.matrix-synapse.database_args setting which + ## by default is matrix-synapse + initialScript = pkgs.writeText "synapse-init.sql" '' + CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD "Da0?H*9i{x?,]|'kq@iBwlIzu"; + CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" + TEMPLATE template0 + LC_COLLATE = "C" + LC_CTYPE = "C"; + ''; + }; + + networking.firewall = let + range = with config.services.coturn; [ { + from = min-port; + to = max-port; + } ]; + in { + enable = true; + allowedUDPPortRanges = range; # coturn + allowedTCPPortRanges = range; + allowedTCPPorts = [ + 22 # SSH + 8448 # Matrix federation + 8008 + 80 + 443 + 3478 # Coturn service + 5349 # Coturn service + 9090 # Synapse Metrics + ]; + allowedUDPPorts = [ + 3478 + 5349 # Coturn service + ]; + }; + + nixpkgs.overlays = [ + (self: super: { + element-web = super.element-web.override { + conf = { + default_server_config = { + "m.homeserver" = { + "base_url" = "https://chat.sealight.xyz"; + "server_name" = "sealight.xyz"; + }; + "m.identity_server" = { + "base_url" = "https://vector.im"; + }; + }; + + ## jitsi will be setup later, + ## but we need to add to Riot configuration + jitsi.preferredDomain = "jitsi.sealight.xyz"; + }; + }; + }) + ]; + + services.nginx = { + enable = true; + virtualHosts = { + + "sealight.xyz" = { + forceSSL = true; + enableACME = true; + locations."/" = { + root = "/var/www/sealight.xyz"; + }; + locations."/_matrix" = { + proxyPass = "http://localhost:8448"; + }; + locations."/slackbridge" = { + proxyPass = "http://localhost:9899"; + }; + }; + + ## virtual host for Synapse + "chat.sealight.xyz" = { + forceSSL = true; + enableACME = true; + #useACMEHost = "sealight.xyz"; + locations."/" = { + proxyPass = "http://localhost:8448"; + }; + }; + + ## virtual host for Riot/Web + "element.sealight.xyz" = { + forceSSL = true; + enableACME = true; + #useACMEHost = "sealight.xyz"; + + ## root points to the riot-web package content, also configured via Nix + locations."/" = { + root = pkgs.element-web; + }; + }; + + ${config.services.jitsi-meet.hostName} = { + enableACME = true; + forceSSL = true; + }; + }; + ## other nginx specific best practices + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedTlsSettings = true; + }; + + environment.systemPackages = with pkgs; [ matrix-appservice-slack ]; +} diff --git a/profiles/helix-zone/mautrix-whatsapp.nix b/profiles/helix-zone/mautrix-whatsapp.nix new file mode 100644 index 00000000..beaaa6f0 --- /dev/null +++ b/profiles/helix-zone/mautrix-whatsapp.nix @@ -0,0 +1,96 @@ +{ lib, config, pkgs, ... }: + +with lib; + +let + cfg = config.services.mautrix-whatsapp; + + configFile = pkgs.runCommand "mautrix-whatsapp" { + buildInputs = [ pkgs.mautrix-whatsapp pkgs.remarshal ]; + preferLocalBuild = true; + } '' + mkdir -p $out + ${pkgs.remarshal}/bin/json2yaml -i ${pkgs.writeText "config.json" (builtins.toJSON cfg.configOptions)} \ + -o $out/config.yaml + + ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp -c $out/config.yaml -g -r $out/registration.yaml + ''; + +in +{ + options.services.mautrix-whatsapp = { + enable = mkEnableOption "Mautrix-whatsapp, a puppeting bridge between Matrix and WhatsApp."; + + configOptions = mkOption { + type = types.attrs; + description = '' + This options will be transform in YAML configuration file for the bridge + + Look here for documentation. + ''; + example = { + configOptions = { + homeserver = { + address = https://matrix.org; + domain = "matrix.org"; + }; + appservice = { + address = http://localhost:8080; + hostname = "0.0.0.0"; + port = 8080; + database = { + type = "sqlite3"; + uri = "/var/lib/mautrix-whatsapp/mautrix-whatsapp.db"; + }; + state_store_path = "/var/lib/mautrix-whatsapp/mx-state.json"; + id = "whatsapp"; + bot = { + username = "whatsappbot"; + displayname = "WhatsApp bridge bot"; + avatar = "mxc://maunium.net/NeXNQarUbrlYBiPCpprYsRqr"; + }; + as_token = ""; + hs_token = ""; + }; + bridge = { + username_template = "whatsapp_{{.}}"; + displayname_template = "{{if .Notify}}{{.Notify}}{{else}}{{.Jid}}{{end}} (WA)"; + command_prefix = "!wa"; + permissions = { + "@example:matrix.org" = 100; + }; + }; + logging = { + directory = "/var/lib/mautrix-whatsapp/logs"; + file_name_format = "{{.Date}}-{{.Index}}.log"; + file_date_format = "\"2006-01-02\""; + file_mode = 384; + timestamp_format = "Jan _2, 2006 15:04:05"; + print_level = "debug"; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.mautrix-whatsapp = { + description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix"; + after = [ "network.target" "matrix-synapse.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + DynamicUser = true; + StateDirectory = "mautrix-whatsapp"; + LoggingDir = "mautrix-whatsapp"; + ExecStart = '' + ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp -c "${configFile}/config.yaml" + ''; + Restart = "on-failure"; + }; + }; + + #services.matrix-synapse.app_service_config_files = [ "${configFile}/registration.yaml" ]; + + }; +} diff --git a/profiles/helix-zone/mod-heisenbridge.nix b/profiles/helix-zone/mod-heisenbridge.nix new file mode 100644 index 00000000..06db08c4 --- /dev/null +++ b/profiles/helix-zone/mod-heisenbridge.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: with lib; let + cfg = config.services.heisenbridge; + + heisenbridgeAppserviceConfig = { + id = "heisenbridge"; + url = "http://${cfg.listenAddress}:${toString cfg.listenPort}"; + as_token = cfg.appServiceToken; + hs_token = cfg.homeserverToken; + rate_limited = false; + sender_localpart = cfg.senderLocalpart; + namespaces = { + users = [{ regex = "@irc_.*"; exclusive = true; }]; + aliases = [ ]; + rooms = [ ]; + }; + }; + + heisenbridgeConfigYaml = pkgs.writeText "heisenbridge.yaml" ( + generators.toYAML { } heisenbridgeAppserviceConfig); +in +{ + options = { + services.heisenbridge = { + enable = mkEnableOption "heisenbridge, a bouncer-style Matrix IRC bridge."; + identd.enable = mkEnableOption "identd for heisenbridge" // { + default = true; + }; + homeserver = mkOption { + type = types.str; + default = "http://localhost:8008"; + description = "The URL of the Matrix homeserver."; + }; + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The address for heisenbridge to listen on."; + }; + listenPort = mkOption { + type = types.int; + default = 9898; + description = "The port for heisenbridge to listen on."; + }; + senderLocalpart = mkOption { + type = types.str; + default = "heisenbridge"; + description = "The localpart of the heisenbridge admin bot's username."; + }; + ownerId = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The owner MXID (for example, @user:homeserver) of the bridge. If + unspecified, the first talking local user will claim the bridge. + ''; + }; + appServiceToken = mkOption { + type = types.str; + description = '' + This is the token that the app service should use as its access_token + when using the Client-Server API. This can be anything you want. + ''; + }; + homeserverToken = mkOption { + type = types.str; + description = '' + This is the token that the homeserver will use when sending requests + to the app service. This can be anything you want. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + meta.maintainers = [ maintainers.sumnerevans ]; + + services.matrix-synapse.app_service_config_files = [ + heisenbridgeConfigYaml + ]; + + # Create a user for heisenbridge. + users.users.heisenbridge = { + group = "heisenbridge"; + isSystemUser = true; + }; + users.groups.heisenbridge = { }; + + # Open ports for identd. + networking.firewall.allowedTCPPorts = mkIf cfg.identd.enable [ 113 ]; + + systemd.services.heisenbridge = { + description = "Heisenbridge Matrix IRC bridge"; + after = ["matrix-synapse.service"]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${pkgs.heisenbridge}/bin/heisenbridge \ + --config ${heisenbridgeConfigYaml} \ + --verbose --verbose \ + --listen-address ${cfg.listenAddress} \ + --listen-port ${toString cfg.listenPort} \ + --uid heisenbridge \ + --gid heisenbridge \ + ${optionalString cfg.identd.enable "--identd"} \ + ${optionalString (cfg.ownerId != null) "--owner ${cfg.ownerId}"} \ + ${cfg.homeserver} + ''; + Restart = "on-failure"; + }; + }; + }; +} + diff --git a/profiles/helix-zone/mod-mx-puppet-slack.nix b/profiles/helix-zone/mod-mx-puppet-slack.nix new file mode 100644 index 00000000..8778f9c7 --- /dev/null +++ b/profiles/helix-zone/mod-mx-puppet-slack.nix @@ -0,0 +1,124 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + dataDir = "/var/lib/mx-puppet-slack"; + registrationFile = "${dataDir}/slack-registration.yaml"; + cfg = config.services.mx-puppet-slack; + settingsFormat = pkgs.formats.json {}; + settingsFile = settingsFormat.generate "mx-puppet-slack-config.json" cfg.settings; + mx-puppet-slack = pkgs.callPackage ./pkg-mx-puppet-slack.nix { }; + +in { + options = { + services.mx-puppet-slack = { + enable = mkEnableOption '' + mx-puppet-slack is a slack puppeting bridge for matrix. + It handles bridging private and group DMs, as well as Guilds (servers) + ''; + + settings = mkOption rec { + apply = recursiveUpdate default; + inherit (settingsFormat) type; + default = { + bridge.port = 8434; + presence = { + enabled = true; + interval = 500; + }; + provisioning.whitelist = [ ]; + relay.whitelist = [ ]; + + # variables are preceded by a colon. + namePatterns = { + user = ":name"; + userOverride = ":displayname"; + room = ":name"; + group = ":name"; + }; + + #defaults to sqlite but can be configured to use postgresql with + #connstring + database.filename = "${dataDir}/database.db"; + logging = { + console = "info"; + lineDateFormat = "MMM-D HH:mm:ss.SSS"; + }; + }; + example = literalExpression '' + { + bridge = { + bindAddress = "localhost"; + domain = "example.com"; + homeserverUrl = "https://example.com"; + }; + + provisioning.whitelist = [ "@admin:example.com" ]; + relay.whitelist = [ "@.*:example.com" ]; + } + ''; + description = '' + config.yaml configuration as a Nix attribute set. + Configuration options should match those described in + + sample.config.yaml. + ''; + }; + serviceDependencies = mkOption { + type = with types; listOf str; + default = optional config.services.matrix-synapse.enable "matrix-synapse.service"; + description = '' + List of Systemd services to require and wait for when starting the application service. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.mx-puppet-slack = { + description = '' + mx-puppet-slack is a slack puppeting bridge for matrix. + It handles bridging private and group DMs, as well as Guilds (servers). + ''; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ] ++ cfg.serviceDependencies; + after = [ "network-online.target" ] ++ cfg.serviceDependencies; + + preStart = '' + # generate the appservice's registration file if absent + if [ ! -f '${registrationFile}' ]; then + ${mx-puppet-slack}/bin/mx-puppet-slack -r -c ${settingsFile} \ + -f ${registrationFile} + fi + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + PrivateTmp = true; + WorkingDirectory = mx-puppet-slack; + StateDirectory = baseNameOf dataDir; + UMask = 0027; + + ExecStart = '' + ${mx-puppet-slack}/bin/mx-puppet-slack \ + -c ${settingsFile} \ + -f ${registrationFile} + ''; + }; + }; + }; + + meta.maintainers = with maintainers; [ chickensoupwithrice ]; +} + diff --git a/profiles/helix-zone/monitoring.nix b/profiles/helix-zone/monitoring.nix new file mode 100644 index 00000000..a487ac77 --- /dev/null +++ b/profiles/helix-zone/monitoring.nix @@ -0,0 +1,159 @@ +{ config, pkgs, ... }: { + # grafana configuration + #services.grafana = { + # enable = true; + # domain = "grafana.mossnet.lan"; + # port = 2342; + # addr = "127.0.0.1"; + #}; + # + ## nginx reverse proxy + #services.nginx.virtualHosts.${config.services.grafana.domain} = { + # locations."/" = { + # proxyPass = "http://127.0.0.1:${toString config.services.grafana.port}"; + # proxyWebsockets = true; + # }; + #}; + + services.prometheus = { + enable = true; + port = 9001; + exporters = { + node = { + enable = true; + enabledCollectors = [ "systemd" ]; + port = 9002; + }; + }; + rules = [ + '' +groups: +- name: synapse + rules: + - record: "synapse_federation_transaction_queue_pendingEdus:total" + expr: "sum(synapse_federation_transaction_queue_pendingEdus or absent(synapse_federation_transaction_queue_pendingEdus)*0)" + - record: "synapse_federation_transaction_queue_pendingPdus:total" + expr: "sum(synapse_federation_transaction_queue_pendingPdus or absent(synapse_federation_transaction_queue_pendingPdus)*0)" + - record: 'synapse_http_server_request_count:method' + labels: + servlet: "" + expr: "sum(synapse_http_server_request_count) by (method)" + - record: 'synapse_http_server_request_count:servlet' + labels: + method: "" + expr: 'sum(synapse_http_server_request_count) by (servlet)' + + - record: 'synapse_http_server_request_count:total' + labels: + servlet: "" + expr: 'sum(synapse_http_server_request_count:by_method) by (servlet)' + + - record: 'synapse_cache:hit_ratio_5m' + expr: 'rate(synapse_util_caches_cache:hits[5m]) / rate(synapse_util_caches_cache:total[5m])' + - record: 'synapse_cache:hit_ratio_30s' + expr: 'rate(synapse_util_caches_cache:hits[30s]) / rate(synapse_util_caches_cache:total[30s])' + + - record: 'synapse_federation_client_sent' + labels: + type: "EDU" + expr: 'synapse_federation_client_sent_edus + 0' + - record: 'synapse_federation_client_sent' + labels: + type: "PDU" + expr: 'synapse_federation_client_sent_pdu_destinations:count + 0' + - record: 'synapse_federation_client_sent' + labels: + type: "Query" + expr: 'sum(synapse_federation_client_sent_queries) by (job)' + + - record: 'synapse_federation_server_received' + labels: + type: "EDU" + expr: 'synapse_federation_server_received_edus + 0' + - record: 'synapse_federation_server_received' + labels: + type: "PDU" + expr: 'synapse_federation_server_received_pdus + 0' + - record: 'synapse_federation_server_received' + labels: + type: "Query" + expr: 'sum(synapse_federation_server_received_queries) by (job)' + + - record: 'synapse_federation_transaction_queue_pending' + labels: + type: "EDU" + expr: 'synapse_federation_transaction_queue_pending_edus + 0' + - record: 'synapse_federation_transaction_queue_pending' + labels: + type: "PDU" + expr: 'synapse_federation_transaction_queue_pending_pdus + 0' + + - record: synapse_storage_events_persisted_by_source_type + expr: sum without(type, origin_type, origin_entity) (synapse_storage_events_persisted_events_sep{origin_type="remote"}) + labels: + type: remote + - record: synapse_storage_events_persisted_by_source_type + expr: sum without(type, origin_type, origin_entity) (synapse_storage_events_persisted_events_sep{origin_entity="*client*",origin_type="local"}) + labels: + type: local + - record: synapse_storage_events_persisted_by_source_type + expr: sum without(type, origin_type, origin_entity) (synapse_storage_events_persisted_events_sep{origin_entity!="*client*",origin_type="local"}) + labels: + type: bridges + - record: synapse_storage_events_persisted_by_event_type + expr: sum without(origin_entity, origin_type) (synapse_storage_events_persisted_events_sep) + - record: synapse_storage_events_persisted_by_origin + expr: sum without(type) (synapse_storage_events_persisted_events_sep) + '' + ]; + scrapeConfigs = [ + { + job_name = "helix"; + static_configs = [{ + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; + }]; + } + { + job_name = "synapse"; + metrics_path = "/_synapse/metrics"; + static_configs = [{ + targets = [ "localhost:9090" ]; + }]; + } + { + job_name = "gitea"; + static_configs = [{ + targets = [ "localhost:3001" ]; + }]; + } + ]; + }; + + networking.firewall = { + enable = true; + allowedTCPPorts = [ 9001 ]; + }; + + services.nginx.virtualHosts."sealight.xyz" = { + locations."/metrics" = { + #basicAuth = { anish = "password"; }; + proxyPass = "http://localhost:9001"; + }; + }; + + services.loki = { + enable = true; + configFile = /var/loki-config.yaml; + }; + + systemd.services.promtail = { + description = "Promtail service for Loki"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = '' + ${pkgs.grafana-loki}/bin/promtail --config.file ${/var/promtail.yaml} + ''; + }; + }; +} diff --git a/profiles/helix-zone/node-composition.nix b/profiles/helix-zone/node-composition.nix new file mode 100644 index 00000000..53bdef1f --- /dev/null +++ b/profiles/helix-zone/node-composition.nix @@ -0,0 +1,17 @@ +# This file has been generated by node2nix 1.9.0. Do not edit! + +{pkgs ? import { + inherit system; + }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs-12_x"}: + +let + nodeEnv = import ./node-env.nix { + inherit (pkgs) stdenv lib python2 runCommand writeTextFile writeShellScript; + inherit pkgs nodejs; + libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null; + }; +in +import ./node-packages.nix { + inherit (pkgs) fetchurl nix-gitignore stdenv lib fetchgit; + inherit nodeEnv; +} diff --git a/profiles/helix-zone/node-env.nix b/profiles/helix-zone/node-env.nix new file mode 100644 index 00000000..5f055785 --- /dev/null +++ b/profiles/helix-zone/node-env.nix @@ -0,0 +1,588 @@ +# This file originates from node2nix + +{lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile, writeShellScript}: + +let + # Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master + utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux; + + python = if nodejs ? python then nodejs.python else python2; + + # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise + tarWrapper = runCommand "tarWrapper" {} '' + mkdir -p $out/bin + + cat > $out/bin/tar <> $out/nix-support/hydra-build-products + ''; + }; + + # Common shell logic + installPackage = writeShellScript "install-package" '' + installPackage() { + local packageName=$1 src=$2 + + local strippedName + + local DIR=$PWD + cd $TMPDIR + + unpackFile $src + + # Make the base dir in which the target dependency resides first + mkdir -p "$(dirname "$DIR/$packageName")" + + if [ -f "$src" ] + then + # Figure out what directory has been unpacked + packageDir="$(find . -maxdepth 1 -type d | tail -1)" + + # Restore write permissions to make building work + find "$packageDir" -type d -exec chmod u+x {} \; + chmod -R u+w "$packageDir" + + # Move the extracted tarball into the output folder + mv "$packageDir" "$DIR/$packageName" + elif [ -d "$src" ] + then + # Get a stripped name (without hash) of the source directory. + # On old nixpkgs it's already set internally. + if [ -z "$strippedName" ] + then + strippedName="$(stripHash $src)" + fi + + # Restore write permissions to make building work + chmod -R u+w "$strippedName" + + # Move the extracted directory into the output folder + mv "$strippedName" "$DIR/$packageName" + fi + + # Change to the package directory to install dependencies + cd "$DIR/$packageName" + } + ''; + + # Bundle the dependencies of the package + # + # Only include dependencies if they don't exist. They may also be bundled in the package. + includeDependencies = {dependencies}: + lib.optionalString (dependencies != []) ( + '' + mkdir -p node_modules + cd node_modules + '' + + (lib.concatMapStrings (dependency: + '' + if [ ! -e "${dependency.name}" ]; then + ${composePackage dependency} + fi + '' + ) dependencies) + + '' + cd .. + '' + ); + + # Recursively composes the dependencies of a package + composePackage = { name, packageName, src, dependencies ? [], ... }@args: + builtins.addErrorContext "while evaluating node package '${packageName}'" '' + installPackage "${packageName}" "${src}" + ${includeDependencies { inherit dependencies; }} + cd .. + ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} + ''; + + pinpointDependencies = {dependencies, production}: + let + pinpointDependenciesFromPackageJSON = writeTextFile { + name = "pinpointDependencies.js"; + text = '' + var fs = require('fs'); + var path = require('path'); + + function resolveDependencyVersion(location, name) { + if(location == process.env['NIX_STORE']) { + return null; + } else { + var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json"); + + if(fs.existsSync(dependencyPackageJSON)) { + var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON)); + + if(dependencyPackageObj.name == name) { + return dependencyPackageObj.version; + } + } else { + return resolveDependencyVersion(path.resolve(location, ".."), name); + } + } + } + + function replaceDependencies(dependencies) { + if(typeof dependencies == "object" && dependencies !== null) { + for(var dependency in dependencies) { + var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency); + + if(resolvedVersion === null) { + process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n"); + } else { + dependencies[dependency] = resolvedVersion; + } + } + } + } + + /* Read the package.json configuration */ + var packageObj = JSON.parse(fs.readFileSync('./package.json')); + + /* Pinpoint all dependencies */ + replaceDependencies(packageObj.dependencies); + if(process.argv[2] == "development") { + replaceDependencies(packageObj.devDependencies); + } + replaceDependencies(packageObj.optionalDependencies); + + /* Write the fixed package.json file */ + fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2)); + ''; + }; + in + '' + node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"} + + ${lib.optionalString (dependencies != []) + '' + if [ -d node_modules ] + then + cd node_modules + ${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies} + cd .. + fi + ''} + ''; + + # Recursively traverses all dependencies of a package and pinpoints all + # dependencies in the package.json file to the versions that are actually + # being used. + + pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args: + '' + if [ -d "${packageName}" ] + then + cd "${packageName}" + ${pinpointDependencies { inherit dependencies production; }} + cd .. + ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} + fi + ''; + + # Extract the Node.js source code which is used to compile packages with + # native bindings + nodeSources = runCommand "node-sources" {} '' + tar --no-same-owner --no-same-permissions -xf ${nodejs.src} + mv node-* $out + ''; + + # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty) + addIntegrityFieldsScript = writeTextFile { + name = "addintegrityfields.js"; + text = '' + var fs = require('fs'); + var path = require('path'); + + function augmentDependencies(baseDir, dependencies) { + for(var dependencyName in dependencies) { + var dependency = dependencies[dependencyName]; + + // Open package.json and augment metadata fields + var packageJSONDir = path.join(baseDir, "node_modules", dependencyName); + var packageJSONPath = path.join(packageJSONDir, "package.json"); + + if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored + console.log("Adding metadata fields to: "+packageJSONPath); + var packageObj = JSON.parse(fs.readFileSync(packageJSONPath)); + + if(dependency.integrity) { + packageObj["_integrity"] = dependency.integrity; + } else { + packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads. + } + + if(dependency.resolved) { + packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided + } else { + packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories. + } + + if(dependency.from !== undefined) { // Adopt from property if one has been provided + packageObj["_from"] = dependency.from; + } + + fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2)); + } + + // Augment transitive dependencies + if(dependency.dependencies !== undefined) { + augmentDependencies(packageJSONDir, dependency.dependencies); + } + } + } + + if(fs.existsSync("./package-lock.json")) { + var packageLock = JSON.parse(fs.readFileSync("./package-lock.json")); + + if(![1, 2].includes(packageLock.lockfileVersion)) { + process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n"); + process.exit(1); + } + + if(packageLock.dependencies !== undefined) { + augmentDependencies(".", packageLock.dependencies); + } + } + ''; + }; + + # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes + reconstructPackageLock = writeTextFile { + name = "addintegrityfields.js"; + text = '' + var fs = require('fs'); + var path = require('path'); + + var packageObj = JSON.parse(fs.readFileSync("package.json")); + + var lockObj = { + name: packageObj.name, + version: packageObj.version, + lockfileVersion: 1, + requires: true, + dependencies: {} + }; + + function augmentPackageJSON(filePath, dependencies) { + var packageJSON = path.join(filePath, "package.json"); + if(fs.existsSync(packageJSON)) { + var packageObj = JSON.parse(fs.readFileSync(packageJSON)); + dependencies[packageObj.name] = { + version: packageObj.version, + integrity: "sha1-000000000000000000000000000=", + dependencies: {} + }; + processDependencies(path.join(filePath, "node_modules"), dependencies[packageObj.name].dependencies); + } + } + + function processDependencies(dir, dependencies) { + if(fs.existsSync(dir)) { + var files = fs.readdirSync(dir); + + files.forEach(function(entry) { + var filePath = path.join(dir, entry); + var stats = fs.statSync(filePath); + + if(stats.isDirectory()) { + if(entry.substr(0, 1) == "@") { + // When we encounter a namespace folder, augment all packages belonging to the scope + var pkgFiles = fs.readdirSync(filePath); + + pkgFiles.forEach(function(entry) { + if(stats.isDirectory()) { + var pkgFilePath = path.join(filePath, entry); + augmentPackageJSON(pkgFilePath, dependencies); + } + }); + } else { + augmentPackageJSON(filePath, dependencies); + } + } + }); + } + } + + processDependencies("node_modules", lockObj.dependencies); + + fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2)); + ''; + }; + + prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}: + let + forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com"; + in + '' + # Pinpoint the versions of all dependencies to the ones that are actually being used + echo "pinpointing versions of dependencies..." + source $pinpointDependenciesScriptPath + + # Patch the shebangs of the bundled modules to prevent them from + # calling executables outside the Nix store as much as possible + patchShebangs . + + # Deploy the Node.js package by running npm install. Since the + # dependencies have been provided already by ourselves, it should not + # attempt to install them again, which is good, because we want to make + # it Nix's responsibility. If it needs to install any dependencies + # anyway (e.g. because the dependency parameters are + # incomplete/incorrect), it fails. + # + # The other responsibilities of NPM are kept -- version checks, build + # steps, postprocessing etc. + + export HOME=$TMPDIR + cd "${packageName}" + runHook preRebuild + + ${lib.optionalString bypassCache '' + ${lib.optionalString reconstructLock '' + if [ -f package-lock.json ] + then + echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!" + echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!" + rm package-lock.json + else + echo "No package-lock.json file found, reconstructing..." + fi + + node ${reconstructPackageLock} + ''} + + node ${addIntegrityFieldsScript} + ''} + + npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild + + if [ "''${dontNpmInstall-}" != "1" ] + then + # NPM tries to download packages even when they already exist if npm-shrinkwrap is used. + rm -f npm-shrinkwrap.json + + npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} install + fi + ''; + + # Builds and composes an NPM package including all its dependencies + buildNodePackage = + { name + , packageName + , version + , dependencies ? [] + , buildInputs ? [] + , production ? true + , npmFlags ? "" + , dontNpmInstall ? false + , bypassCache ? false + , reconstructLock ? false + , preRebuild ? "" + , dontStrip ? true + , unpackPhase ? "true" + , buildPhase ? "true" + , meta ? {} + , ... }@args: + + let + extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ]; + in + stdenv.mkDerivation ({ + name = "${name}-${version}"; + buildInputs = [ tarWrapper python nodejs ] + ++ lib.optional (stdenv.isLinux) utillinux + ++ lib.optional (stdenv.isDarwin) libtool + ++ buildInputs; + + inherit nodejs; + + inherit dontStrip; # Stripping may fail a build for some package deployments + inherit dontNpmInstall preRebuild unpackPhase buildPhase; + + compositionScript = composePackage args; + pinpointDependenciesScript = pinpointDependenciesOfPackage args; + + passAsFile = [ "compositionScript" "pinpointDependenciesScript" ]; + + installPhase = '' + source ${installPackage} + + # Create and enter a root node_modules/ folder + mkdir -p $out/lib/node_modules + cd $out/lib/node_modules + + # Compose the package and all its dependencies + source $compositionScriptPath + + ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }} + + # Create symlink to the deployed executable folder, if applicable + if [ -d "$out/lib/node_modules/.bin" ] + then + ln -s $out/lib/node_modules/.bin $out/bin + fi + + # Create symlinks to the deployed manual page folders, if applicable + if [ -d "$out/lib/node_modules/${packageName}/man" ] + then + mkdir -p $out/share + for dir in "$out/lib/node_modules/${packageName}/man/"* + do + mkdir -p $out/share/man/$(basename "$dir") + for page in "$dir"/* + do + ln -s $page $out/share/man/$(basename "$dir") + done + done + fi + + # Run post install hook, if provided + runHook postInstall + ''; + + meta = { + # default to Node.js' platforms + platforms = nodejs.meta.platforms; + } // meta; + } // extraArgs); + + # Builds a node environment (a node_modules folder and a set of binaries) + buildNodeDependencies = + { name + , packageName + , version + , src + , dependencies ? [] + , buildInputs ? [] + , production ? true + , npmFlags ? "" + , dontNpmInstall ? false + , bypassCache ? false + , reconstructLock ? false + , dontStrip ? true + , unpackPhase ? "true" + , buildPhase ? "true" + , ... }@args: + + let + extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ]; + in + stdenv.mkDerivation ({ + name = "node-dependencies-${name}-${version}"; + + buildInputs = [ tarWrapper python nodejs ] + ++ lib.optional (stdenv.isLinux) utillinux + ++ lib.optional (stdenv.isDarwin) libtool + ++ buildInputs; + + inherit dontStrip; # Stripping may fail a build for some package deployments + inherit dontNpmInstall unpackPhase buildPhase; + + includeScript = includeDependencies { inherit dependencies; }; + pinpointDependenciesScript = pinpointDependenciesOfPackage args; + + passAsFile = [ "includeScript" "pinpointDependenciesScript" ]; + + installPhase = '' + source ${installPackage} + + mkdir -p $out/${packageName} + cd $out/${packageName} + + source $includeScriptPath + + # Create fake package.json to make the npm commands work properly + cp ${src}/package.json . + chmod 644 package.json + ${lib.optionalString bypassCache '' + if [ -f ${src}/package-lock.json ] + then + cp ${src}/package-lock.json . + fi + ''} + + # Go to the parent folder to make sure that all packages are pinpointed + cd .. + ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} + + ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }} + + # Expose the executables that were installed + cd .. + ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} + + mv ${packageName} lib + ln -s $out/lib/node_modules/.bin $out/bin + ''; + } // extraArgs); + + # Builds a development shell + buildNodeShell = + { name + , packageName + , version + , src + , dependencies ? [] + , buildInputs ? [] + , production ? true + , npmFlags ? "" + , dontNpmInstall ? false + , bypassCache ? false + , reconstructLock ? false + , dontStrip ? true + , unpackPhase ? "true" + , buildPhase ? "true" + , ... }@args: + + let + nodeDependencies = buildNodeDependencies args; + in + stdenv.mkDerivation { + name = "node-shell-${name}-${version}"; + + buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs; + buildCommand = '' + mkdir -p $out/bin + cat > $out/bin/shell < $out/bin/mx-puppet-slack + #!/bin/sh + exec ${nodejs}/bin/node $out/lib/node_modules/mx-puppet-slack/build/index.js "\$@" + EOF + chmod +x $out/bin/mx-puppet-slack + ''; + + meta = with lib; { + description = "A slack puppeting bridge for matrix"; + license = licenses.asl20; + homepage = "https://github.com/Sorunome/mx-puppet-slack"; + maintainers = with maintainers; [ chickensoupwithrice ]; + platforms = platforms.unix; + }; +} + diff --git a/profiles/helix-zone/rss-bridge.nix b/profiles/helix-zone/rss-bridge.nix new file mode 100644 index 00000000..dee16e01 --- /dev/null +++ b/profiles/helix-zone/rss-bridge.nix @@ -0,0 +1,11 @@ +{pkgs, lib, config, ...}: +{ + services.rss-bridge = { + enable = true; + virtualHost = "bridge.sealight.xyz"; + whitelist = [ "Facebook" "Bandcamp" "Twitter" "Telegram" "Instagram" "Reddit"]; + }; + + services.nginx.virtualHosts."bridge.sealight.xyz".forceSSL = true; + services.nginx.virtualHosts."bridge.sealight.xyz".enableACME = true; +} diff --git a/profiles/helix-zone/sealight.nix b/profiles/helix-zone/sealight.nix new file mode 100644 index 00000000..ed882042 --- /dev/null +++ b/profiles/helix-zone/sealight.nix @@ -0,0 +1,14 @@ +{ + services.nginx = { + enable = true; + vhost = { + "sealight.xyz" = { + forceSSL = true; + enableACME = true; + locations."/" = { + root = "/var/www/sealight.xyz" + }; + }; + }; + }; +} diff --git a/profiles/helix-zone/wireguard.nix b/profiles/helix-zone/wireguard.nix new file mode 100644 index 00000000..a74ab802 --- /dev/null +++ b/profiles/helix-zone/wireguard.nix @@ -0,0 +1,32 @@ +{ config, pkgs, lib, ... }: +{ + environment.systemPackages = [ pkgs.wireguard pkgs.mosh ]; + networking.nat = { + enable = true; + externalInterface = "ens3"; + internalInterfaces = [ "wg0" ]; + }; + networking.firewall = { + allowedUDPPorts = [ 60990 ]; + }; + + networking.wireguard.interfaces.wg0 = { + ips = [ "10.0.69.1/24" ]; + listenPort = 60990; + privateKeyFile = "/var/lib/wireguard/priv"; + peers = [ + { # box + publicKey = "Ra78mOc110K7URN5uB3m9d78iBKgeRHzT+3HkiFp9BU="; + allowedIPs = [ "10.0.69.4/32" ]; + } + { # line + publicKey = "fsb1FuGmYi89p4CVXytKw2FYamONKtoanxrgs/dJjC8="; + allowedIPs = [ "10.0.69.3/32" ]; + } + { # curve + publicKey = "XmV4AcPPND/eeSI6d9M1iahCjtymqY6DdUDEFNP/x3g="; + allowedIPs = [ "10.0.69.2/32" ]; + } +]; + }; +}