diff --git a/depot/hosts/beltane/nixos.nix b/depot/hosts/beltane/nixos.nix index 4f70de00..91c08e00 100644 --- a/depot/hosts/beltane/nixos.nix +++ b/depot/hosts/beltane/nixos.nix @@ -9,9 +9,11 @@ with lib; profiles.hardware.rm-310 profiles.gui users.kat.guiFull + services.fusionpbx services.jellyfin services.kattv-ingest services.promtail + services.postgres services.netdata services.nfs services.nginx diff --git a/depot/modules/nixos/default.nix b/depot/modules/nixos/default.nix index de373226..5fd5222c 100644 --- a/depot/modules/nixos/default.nix +++ b/depot/modules/nixos/default.nix @@ -4,6 +4,7 @@ imports = with (import (sources.nixexprs + "/modules")).nixos; [ base16 base16-shared modprobe ] ++ [ ./nftables.nix ./firewall.nix + ./fusionpbx.nix ./deploy.nix ./dyndns.nix ./network.nix diff --git a/depot/modules/nixos/fusionpbx.nix b/depot/modules/nixos/fusionpbx.nix new file mode 100644 index 00000000..ce077ba4 --- /dev/null +++ b/depot/modules/nixos/fusionpbx.nix @@ -0,0 +1,413 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.fusionpbx; + toKeyValue = generators.toKeyValue { + mkKeyValue = generators.mkKeyValueDefault {} " = "; + }; + php = "${pkgs.php74}/bin/php"; + psql_base = "${pkgs.postgresql_11}/bin/psql"; + psql = if ! cfg.useLocalPostgreSQL then + "${psql_base} --host=${cfg.postgres.host} --port=${cfg.postgres.port} --username=${cfg.postgres.db_username}" + else psql_base; + freeSwitchConfig = pkgs.writeShellScriptBin "copy_config" '' + set -exu + if [ ! -f "${cfg.home}/state/installed" ]; then + mkdir -p /etc/freeswitch + cp --no-preserve=mode,ownership -r ${cfg.package}/resources/templates/conf/* /etc/freeswitch + fi + ''; + installerReplacement = pkgs.writeShellScriptBin "installer_replacement" '' + set -exu + + if [ -f "${cfg.home}/state/installed" ]; then + if [ -f "${cfg.home}/state/lastversion" ]; then + lastversion=$(<${cfg.home}/state/lastversion) + if [ lastversion != "${cfg.package.version}" ]; then + ${php} ${cfg.package}/core/upgrade/upgrade_schema.php + echo "${cfg.package.version}" >| ${cfg.home}/state/lastversion + fi + fi + else + mkdir -p /var/lib/fusionpbx + + ${if ! cfg.useLocalPostgreSQL then "PGPASSWORD=${cfg.postgres.db_password}" else ""} + ${php} ${cfg.package}/core/upgrade/upgrade_schema.php + + domain_uuid=$(${php} ${cfg.package}/resources/uuid.php); + domain_name=${cfg.domain} + ${psql} -c "insert into v_domains (domain_uuid, domain_name, domain_enabled) values('$domain_uuid', '$domain_name', 'true');" + cd "${cfg.package}" && ${php} ${cfg.package}/core/upgrade/upgrade_domains.php + + user_uuid=$(${php} ${cfg.package}/resources/uuid.php); + user_salt=$(${php} ${cfg.package}/resources/uuid.php); + + password_hash=$(${php} -r "echo md5('$user_salt$USER_PASSWORD');"); + ${psql} -t -c "insert into v_users (user_uuid, domain_uuid, username, password, salt, user_enabled) values('$user_uuid', '$domain_uuid', '$USER_NAME', '$password_hash', '$user_salt', 'true');" + + group_uuid=$(${psql} -qtAX -c "select group_uuid from v_groups where group_name = 'superadmin';"); + group_uuid=$(echo $group_uuid | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//') +user_group_uuid=$(${php} ${cfg.package}/resources/uuid.php); + group_name=superadmin + #echo "insert into v_user_groups (user_group_uuid, domain_uuid, group_name, group_uuid, user_uuid) values('$user_group_uuid', '$domain_uuid', '$group_name', '$group_uuid', '$user_uuid');" + ${psql} -c "insert into v_user_groups (user_group_uuid, domain_uuid, group_name, group_uuid, user_uuid) values('$user_group_uuid', '$domain_uuid', '$group_name', '$group_uuid', '$user_uuid');" + + xml_cdr_username=$(dd if=/dev/urandom bs=1 count=20 2>/dev/null | base64 | sed 's/[=\+//]//g') + xml_cdr_password=$(dd if=/dev/urandom bs=1 count=20 2>/dev/null | base64 | sed 's/[=\+//]//g') + sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_http_protocol}:http:" + sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_project_path}::" + sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_user}:$xml_cdr_username:" + sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_pass}:$xml_cdr_password:" + + cd "${cfg.package}" && ${php} ${cfg.package}/core/upgrade/upgrade_domains.php + + mkdir -p ${cfg.home}/state + touch ${cfg.home}/state/installed + fi + ''; +in { + options.services.fusionpbx = { + enable = mkEnableOption "Enable FusionPBX"; + openFirewall = mkEnableOption "Open the firewall for FusionPBX" // { default = true; }; + useLocalPostgreSQL = mkEnableOption "Use Local PostgreSQL for FusionPBX" // { default = true; }; + postgres = { + host = mkOption { + type = types.nullOr types.str; + default = null; + }; + port = mkOption { + type = types.nullOr types.port; + default = null; + }; + db_name = mkOption { + type = types.nullOr types.str; + default = null; + }; + db_username = mkOption { + type = types.nullOr types.str; + default = null; + }; + db_password = mkOption { + type = types.nullOr types.str; + default = null; + }; + }; + + environmentFile = mkOption { + type = types.str; + example = '' + USER_NAME="meow" + USER_PASSWORD="nya" + ''; + }; + + hardphones = mkEnableOption "Are you going to use hardphones with FusionPBX?"; + useWebrootACME = mkEnableOption "Do you want webroot-style ACME cert generation?"; + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + }; + + domain = mkOption { + type = types.str; + }; + + package = mkOption { + type = types.package; + description = "What package to use for FusionPBX?"; + default = pkgs.fusionpbx; + relatedPackages = [ + "fusionpbx" + ]; + }; + + freeSwitchPackage = mkOption { + type = types.package; + description = "What package to use for FreeSWITCH?"; + default = pkgs.freeswitch; + relatedPackages = [ + "freeswitch" + ]; + }; + + home = mkOption { + type = types.str; + default = "/var/lib/fusionpbx"; + description = "Storage path for FusionPBX"; + }; + }; + + config = mkIf cfg.enable { + # User & Group Definition + users.users.fusionpbx = { + home = cfg.home; + group = "fusionpbx"; + createHome = true; + isSystemUser = true; + }; + users.groups.fusionpbx.members = [ + "fusionpbx" + config.services.nginx.user + ]; + + # PostgreSQL + services.postgresql = mkIf cfg.useLocalPostgreSQL { + ensureUsers = [ + { + name = "fusionpbx"; + ensurePermissions = { + "DATABASE fusionpbx" = "ALL PRIVILEGES"; + }; + } + { + name = "freeswitch"; + ensurePermissions = { + "DATABASE fusionpbx" = "ALL PRIVILEGES"; + "DATABASE freeswitch" = "ALL PRIVILEGES"; + }; + } + ]; + ensureDatabases = [ "fusionpbx" "freeswitch" ]; + }; + + # ACME + security.acme.certs = mkIf cfg.useWebrootACME { + ${cfg.domain} = { + group = "fusionpbx"; + }; + }; + + # NGINX + services.nginx = { + enable = mkDefault true; + virtualHosts.${cfg.domain} = { + enableACME = cfg.useWebrootACME; + useACMEHost = cfg.useACMEHost; + forceSSL = true; + # forceSSL = true; # This might not make sense due to SSL-incapable hardphones? + root = cfg.package; + locations = { + "/" = { + index = "index.php"; + }; + "~ .htaccess".extraConfig = "deny all;"; + "~ .htpassword".extraConfig = "deny all;"; + "~^.+.(db)$".extraConfig = "deny all;"; + "~ \\.php$" = { + extraConfig = '' + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_pass unix:${config.services.phpfpm.pools.fusionpbx.socket}; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME ${cfg.package}$fastcgi_script_name; + ''; + }; + " = /core/upgrade/index.php".extraConfig = '' + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_pass unix:${config.services.phpfpm.pools.fusionpbx.socket}; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME ${cfg.package}$fastcgi_script_name; + fastcgi_read_timeout 15m; + ''; + }; + /* + if ($uri !~* ^.*(provision|xml_cdr).*$) { + rewrite ^(.*) https://$host$1 permanent; + break; + } + */ + extraConfig = '' + client_max_body_size 80M; + client_body_buffer_size 128k; + + + #REST api + if ($uri ~* ^.*/api/.*$) { + rewrite ^(.*)/api/(.*)$ $1/api/index.php?rewrite_uri=$2 last; + break; + } + '' + optionalString cfg.hardphones '' + #algo + rewrite "^.*/provision/algom([A-Fa-f0-9]{12})(\.(conf))?$" /app/provision/?mac=$1; + + #mitel + rewrite "^.*/provision/MN_([A-Fa-f0-9]{12})\.cfg" /app/provision/index.php?mac=$1&file=MN_%7b%24mac%7d.cfg last; + rewrite "^.*/provision/MN_Generic.cfg" /app/provision/index.php?mac=08000f000000&file=MN_Generic.cfg last; + + #grandstream + rewrite "^.*/provision/cfg([A-Fa-f0-9]{12})(\.(xml|cfg))?$" /app/provision/?mac=$1; + rewrite "^.*/provision/pb([A-Fa-f0-9-]{12,17})/phonebook\.xml$" /app/provision/?mac=$1&file=phonebook.xml; + #grandstream-wave softphone by ext because Android doesn't pass MAC. + rewrite "^.*/provision/([0-9]{5})/cfg([A-Fa-f0-9]{12}).xml$" /app/provision/?ext=$1; + + #aastra + rewrite "^.*/provision/aastra.cfg$" /app/provision/?mac=$1&file=aastra.cfg; + #rewrite "^.*/provision/([A-Fa-f0-9]{12})(\.(cfg))?$" /app/provision/?mac=$1 last; + + #yealink common + rewrite "^.*/provision/(y[0-9]{12})(\.cfg)?$" /app/provision/index.php?file=$1.cfg; + + #yealink mac + rewrite "^.*/provision/([A-Fa-f0-9]{12})(\.(xml|cfg))?$" /app/provision/index.php?mac=$1 last; + + #polycom + rewrite "^.*/provision/000000000000.cfg$" "/app/provision/?mac=$1&file={%24mac}.cfg"; + #rewrite "^.*/provision/sip_330(\.(ld))$" /includes/firmware/sip_330.$2; + rewrite "^.*/provision/features.cfg$" /app/provision/?mac=$1&file=features.cfg; + rewrite "^.*/provision/([A-Fa-f0-9]{12})-sip.cfg$" /app/provision/?mac=$1&file=sip.cfg; + rewrite "^.*/provision/([A-Fa-f0-9]{12})-phone.cfg$" /app/provision/?mac=$1; + rewrite "^.*/provision/([A-Fa-f0-9]{12})-registration.cfg$" "/app/provision/?mac=$1&file={%24mac}-registration.cfg"; + rewrite "^.*/provision/([A-Fa-f0-9]{12})-directory.xml$" "/app/provision/?mac=$1&file={%24mac}-directory.xml"; + + #cisco + rewrite "^.*/provision/file/(.*\.(xml|cfg))" /app/provision/?file=$1 last; + + #Escene + rewrite "^.*/provision/([0-9]{1,11})_Extern.xml$" "/app/provision/?ext=$1&file={%24mac}_extern.xml" last; + rewrite "^.*/provision/([0-9]{1,11})_Phonebook.xml$" "/app/provision/?ext=$1&file={%24mac}_phonebook.xml" last; + + #Vtech + rewrite "^.*/provision/VCS754_([A-Fa-f0-9]{12})\.cfg$" /app/provision/?mac=$1; + rewrite "^.*/provision/pb([A-Fa-f0-9-]{12,17})/directory\.xml$" /app/provision/?mac=$1&file=directory.xml; + + #Digium + rewrite "^.*/provision/([A-Fa-f0-9]{12})-contacts\.cfg$" "/app/provision/?mac=$1&file={%24mac}-contacts.cfg"; + rewrite "^.*/provision/([A-Fa-f0-9]{12})-smartblf\.cfg$" "/app/provision/?mac=$1&file={%24mac}-smartblf.cfg"; + ''; + }; + }; + + # PHP 7.4 + services.phpfpm = { + pools.fusionpbx = { + user = "fusionpbx"; + group = "fusionpbx"; + phpEnv = { + PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; + }; + settings = { + "pm" = "dynamic"; + "pm.max_children" = "32"; + "pm.start_servers" = "2"; + "pm.min_spare_servers" = "2"; + "pm.max_spare_servers" = "4"; + "pm.max_requests" = "500"; + "listen.owner" = "fusionpbx"; + "listen.group" = config.services.nginx.group; + }; + phpPackage = pkgs.php74.buildEnv { + extensions = { enabled, all }: ( + with all; + enabled ++ [ + imap + pgsql + curl + opcache + pdo + pdo_pgsql + soap + xmlrpc + gd + ] + ); + extraConfig = toKeyValue { + }; + }; + }; + }; + + # FreeSWITCH + systemd.tmpfiles.rules = [ + "v /etc/freeswitch 5777 fusionpbx fusionpbx" + ]; + + systemd.services.freeswitch = let + pkg = cfg.freeSwitchPackage; + configPath = "/etc/freeswitch"; + in { + description = "Free and open-source application server for real-time communication"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "fusionpbx"; + Group = "fusionpbx"; + StateDirectory = "freeswitch"; + ExecStartPre = "${freeSwitchConfig}/bin/copy_config"; + ExecStart = "${pkg}/bin/freeswitch -nf \\ + -mod ${pkg}/lib/freeswitch/mod \\ + -conf ${configPath} \\ + -base /var/lib/freeswitch"; + ExecReload = "${pkg}/bin/fs_cli -x reloadxml"; + Restart = "on-failure"; + RestartSec = "5s"; + CPUSchedulingPolicy = "fifo"; + }; + }; + + systemd.services.fusionpbx = { + after = [ "network.target" ]; + wantedBy = [ "freeswitch.service" ]; + script = "${installerReplacement}/bin/installer_replacement"; + serviceConfig = { + EnvironmentFile = cfg.environmentFile; + User = "fusionpbx"; + Group = "fusionpbx"; + Type = "oneshot"; + StateDirectory = "fusionpbx"; + }; + }; + + # FusionPBX Config + environment.etc."fusionpbx/config.php" = { + user = "nginx"; + group = "fusionpbx"; + text = let + hostConfig = if cfg.useLocalPostgreSQL then '' + $db_type = 'pgsql'; + $db_host = '''; + $db_port = '''; + $db_name = 'fusionpbx'; + $db_username = 'fusionpbx'; + $db_password = '''; + '' else '' + $db_type = 'pgsql'; + $db_host = '${cfg.postgres.host}'; + $db_port = '${toString cfg.postgres.port}'; + $db_name = '${cfg.postgres.db_name}'; + $db_username = '${cfg.postgres.db_username}'; + $db_password = '${cfg.postgres.db_password}'; + ''; in '' + + ''; + }; + + # Firewall + network.firewall = mkIf cfg.openFirewall { + public = { + tcp = { + ports = [ 5060 5160 ]; + ranges = [ + { + from = 10000; + to = 20000; + } + ]; + }; + udp = { + ports = [ 5060 5160 ]; + ranges = [ + { + from = 10000; + to = 20000; + } + ]; + }; + }; + }; + }; +} diff --git a/depot/services/fusionpbx/default.nix b/depot/services/fusionpbx/default.nix new file mode 100644 index 00000000..ca08ccad --- /dev/null +++ b/depot/services/fusionpbx/default.nix @@ -0,0 +1,43 @@ +{ config, pkgs, tf, ... }: + +{ + deploy.tf.dns.records.services_fusionpbx = { + tld = config.network.dns.tld; + domain = "pbx"; + cname.target = "${config.network.addresses.private.domain}."; + }; + + kw.secrets = [ + "fusionpbx-username" + "fusionpbx-password" + ]; + + secrets.files.fusionpbx_env = { + text = '' + USER_NAME=${tf.variables.fusionpbx-username.ref} + USER_PASSWORD=${tf.variables.fusionpbx-password.ref} + ''; + owner = "fusionpbx"; + group = "fusionpbx"; + }; + + security.acme.certs.services_fusionpbx = { + domain = "pbx.${config.network.dns.domain}"; + group = "fusionpbx"; + dnsProvider = "rfc2136"; + credentialsFile = config.secrets.files.dns_creds.path; + postRun = "systemctl restart nginx"; + }; + + services.fusionpbx = { + enable = true; + openFirewall = true; + useLocalPostgreSQL = true; + environmentFile = config.secrets.files.fusionpbx_env.path; + hardphones = true; + useACMEHost = "services_fusionpbx"; + domain = "pbx.${config.network.dns.domain}"; + package = with pkgs; fusionpbx; + freeSwitchPackage = with pkgs; freeswitch; + }; +} diff --git a/pkgs/default.nix b/pkgs/default.nix index ab29d90b..7edd0660 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -90,6 +90,8 @@ let kat-scrot = self.callPackage ./kat-scrot { }; + fusionpbx = self.callPackage ./fusionpbx { }; + } // super.lib.optionalAttrs (builtins.pathExists ../config/trusted/pkgs) (import ../config/trusted/pkgs { inherit super self; }); pkgs = import sources.nixpkgs { diff --git a/pkgs/fusionpbx/default.nix b/pkgs/fusionpbx/default.nix new file mode 100644 index 00000000..e4ee7052 --- /dev/null +++ b/pkgs/fusionpbx/default.nix @@ -0,0 +1,30 @@ +{ + lib, + stdenv, + fetchFromGitHub +}: + +stdenv.mkDerivation rec { + pname = "fusionpbx"; + version = "master"; + + src = fetchFromGitHub { + owner = pname; + repo = pname; + rev = "2b8d011321ee5f2ffba967e38fcc8c542f378502"; + sha256 = "0fsmf67hrddz6aqjrjjqxa72iw108z2skwhn9jb3p465xfq7a9ij"; + }; + + installPhase = '' + mkdir -p $out + mv * $out + ''; + + meta = with lib; { + description = "A full-featured domain based multi-tenant PBX and voice switch for FreeSWITCH."; + homepage = "https://www.fusionpbx.com/"; + license = with licenses; mpl11; + maintainers = with maintainers; [ kittywitch ]; + platforms = with platforms; unix; + }; +}