{ lib, pkgs, config, ... }: let cfg = config.services.voip; phones = import ./phones.nix { inherit lib; }; inherit (phones) models; allPhones = phones.mkAllPhones cfg; intercomEntries = phones.mkIntercomEntries cfg allPhones; mohDirs = import ./asterisk/moh.nix { inherit lib pkgs cfg; }; greetingDirs = import ./asterisk/greetings.nix { inherit lib pkgs cfg; }; confFiles = import ./asterisk/default.nix { inherit lib cfg models allPhones intercomEntries mohDirs greetingDirs; }; directory = import ./provisioning/directory.nix { inherit lib pkgs cfg allPhones intercomEntries; }; directoryJson = import ./provisioning/directory-json.nix { inherit lib pkgs cfg models allPhones; }; provisioningRoot = import ./provisioning/default.nix { inherit lib pkgs cfg models allPhones; }; diagram = import ./dashboard.nix { inherit lib pkgs cfg models allPhones intercomEntries; }; # True when any *File option is set — Asterisk's execincludes=yes is required in that case. hasRuntimeSecrets = lib.any (t: t.hostFile != null || t.usernameFile != null || t.passwordFile != null || t.callerIdFile != null) (lib.attrValues cfg.sipTrunks) || lib.any (d: d.numberFile != null) (lib.attrValues cfg.dids); # Nginx Lua handler: reads the static HTML template and substitutes every # @@/path/to/keyfile@@ marker with the file's first line at request time. luaPageHandler = pkgs.writeText "voip-page.lua" '' local f = assert(io.open("${diagram.webRoot}/index.html", "rb")) local html = f:read("*a") f:close() -- Placeholders embed the full key file path: @@/var/lib/voip-keys/name@@ html = html:gsub("@@([^@]+)@@", function(path) local kf = io.open(path, "r") if not kf then return "(not yet uploaded)" end local val = kf:read("*l") kf:close() return val or "" end) ngx.header.content_type = "text/html; charset=utf-8" ngx.print(html) ''; in { imports = [ ./options.nix ]; config = lib.mkIf cfg.enable { services.voip.ntpServer = lib.mkDefault cfg.serverAddress; assertions = import ./assertions.nix { inherit lib cfg models allPhones; }; services.asterisk = { enable = true; confFiles = confFiles; # execincludes=yes is required when any *File option is in use. extraConfig = lib.optionalString hasRuntimeSecrets '' [options] execincludes=yes ''; }; services.atftpd = { enable = true; root = "${provisioningRoot}"; extraOptions = [ "--verbose=7" ]; }; services.nginx = { enable = true; # OpenResty bundles nginx + LuaJIT + resty.core and all required libraries. # Needed for request-time secret substitution in the status page. package = lib.mkIf hasRuntimeSecrets (lib.mkDefault pkgs.openresty); # Cisco phones fetch provisioning files (SEP*.cnf.xml, dialplan-*.xml, # backgrounds) over TFTP (primary) and HTTP port 6970 (fallback). # Both serve the same Nix-built provisioning root. virtualHosts."voip-provisioning" = { listen = [{ addr = "0.0.0.0"; port = 6970; }]; locations."/" = { root = "${provisioningRoot}"; extraConfig = "autoindex off;"; }; }; virtualHosts."voip-directory" = { listen = [{ addr = "0.0.0.0"; port = cfg.directoryPort; }]; locations = { "= /directory.xml" = { alias = "${directory.menuFile}"; extraConfig = "default_type text/xml;"; }; "= /directory-list.xml" = { alias = "${directory.listFile}"; extraConfig = "default_type text/xml;"; }; "= /intercom.xml" = { alias = "${directory.intercomFile}"; extraConfig = "default_type text/xml;"; }; "= /contacts.json" = { alias = "${directoryJson}"; extraConfig = ''default_type application/json; add_header Access-Control-Allow-Origin "*";''; }; "/" = { root = "${diagram.webRoot}"; extraConfig = lib.optionalString (!hasRuntimeSecrets) "index index.html;"; }; } // lib.optionalAttrs hasRuntimeSecrets { # Exact-match the index so the Lua handler intercepts it before the # prefix location /. Other assets (voip.dot, SVG) fall through to /. "= /" = { extraConfig = "default_type text/html;\ncontent_by_lua_file ${luaPageHandler};"; }; "= /index.html" = { extraConfig = "default_type text/html;\ncontent_by_lua_file ${luaPageHandler};"; }; }; }; }; # voip-keys group: both asterisk (#exec reads) and nginx (Lua reads) need access. # Key files must be deployed with group = "voip-keys" and permissions = "0640". users.groups.voip-keys = {}; users.users.asterisk.extraGroups = [ "voip-keys" ]; users.users.nginx.extraGroups = [ "voip-keys" ]; systemd.tmpfiles.rules = [ "d /var/log/nginx 0750 nginx nginx -" ]; networking.firewall = { allowedTCPPorts = [ cfg.sipPort cfg.directoryPort 6970 ]; allowedUDPPorts = [ cfg.sipPort 69 ]; allowedUDPPortRanges = [{ from = cfg.rtpStart; to = cfg.rtpEnd; }]; }; }; }