summaryrefslogtreecommitdiffstats
path: root/nixos/modules/services/networking/hylafax
diff options
context:
space:
mode:
authorYarny0 <41838844+Yarny0@users.noreply.github.com>2018-08-04 17:08:54 +0200
committerYarny0 <41838844+Yarny0@users.noreply.github.com>2018-09-08 14:21:40 +0200
commit12fa95f2d696b6babb365a27efef140e7113cc34 (patch)
tree85046edaf3247f85cce2dad2e31c56c881d2422c /nixos/modules/services/networking/hylafax
parenta08b633fe7b636556ece29cc75448254b6ac3d28 (diff)
modules: HylaFAX server configuration
This commit adds the following * the uucp user * options for HylaFAX server to control startup and modems * systemd services for HylaFAX server processes including faxgettys for modems * systemd services to maintain the HylaFAX spool area, including cleanup with faxcron and faxqclean * default configuration for all server processes for a minimal working configuration Some notes: * HylaFAX configuration cannot be initialized with faxsetup (as it would be common on other Linux distributions). The hylafaxplus package contains a template spool area. * Modems are controlled by faxgetty. Send-only configuration (modems controlled by faxq) is not supported by this configuration setup. * To enable the service, one or more modems must be defined with config.services.hylafax.modems . * Sending mail *should* work: HylaFAX will use whatever is in config.services.mail.sendmailSetuidWrapper.program unless overridden with the sendmailPath option. * The admin has to create a hosts.hfaxd file somewhere (e.g. in /etc) before enabling HylaFAX. This file controls access to the server (see hosts.hfaxd(5) ). Sadly, HylaFAX does not permit account-based access control as is accepts connections via TCP only. * Active fax polling should work; I can't test it. * Passive fax polling is not supported by HylaFAX. * Pager transmissions (with sendpage) are disabled by default. I have never tested or used these. * Incoming data/voice/"extern"al calls won't be handled by default. I have never tested or used these.
Diffstat (limited to 'nixos/modules/services/networking/hylafax')
-rw-r--r--nixos/modules/services/networking/hylafax/default.nix29
-rw-r--r--nixos/modules/services/networking/hylafax/faxq-default.nix12
-rwxr-xr-xnixos/modules/services/networking/hylafax/faxq-wait.sh29
-rw-r--r--nixos/modules/services/networking/hylafax/hfaxd-default.nix10
-rw-r--r--nixos/modules/services/networking/hylafax/modem-default.nix22
-rw-r--r--nixos/modules/services/networking/hylafax/options.nix375
-rwxr-xr-xnixos/modules/services/networking/hylafax/spool.sh111
-rw-r--r--nixos/modules/services/networking/hylafax/systemd.nix249
8 files changed, 837 insertions, 0 deletions
diff --git a/nixos/modules/services/networking/hylafax/default.nix b/nixos/modules/services/networking/hylafax/default.nix
new file mode 100644
index 000000000000..6e28e8c5d8bc
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/default.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+{
+
+ imports = [
+ ./options.nix
+ ./systemd.nix
+ ];
+
+ config = lib.modules.mkIf config.services.hylafax.enable {
+ environment.systemPackages = [ pkgs.hylafaxplus ];
+ users.users.uucp = {
+ uid = config.ids.uids.uucp;
+ group = "uucp";
+ description = "Unix-to-Unix CoPy system";
+ isSystemUser = true;
+ inherit (config.users.users.nobody) home;
+ };
+ assertions = [{
+ assertion = config.services.hylafax.modems != {};
+ message = ''
+ HylaFAX cannot be used without modems.
+ Please define at least one modem with
+ <option>config.services.hylafax</option>.
+ '';
+ }];
+ };
+
+}
diff --git a/nixos/modules/services/networking/hylafax/faxq-default.nix b/nixos/modules/services/networking/hylafax/faxq-default.nix
new file mode 100644
index 000000000000..a2630ce66b71
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/faxq-default.nix
@@ -0,0 +1,12 @@
+{ ... }:
+
+# see man:hylafax-config(5)
+
+{
+
+ ModemGroup = [ ''"any:.*"'' ];
+ ServerTracing = "0x78701";
+ SessionTracing = "0x78701";
+ UUCPLockDir = "/var/lock";
+
+}
diff --git a/nixos/modules/services/networking/hylafax/faxq-wait.sh b/nixos/modules/services/networking/hylafax/faxq-wait.sh
new file mode 100755
index 000000000000..8c39e9d20c18
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/faxq-wait.sh
@@ -0,0 +1,29 @@
+#! @shell@ -e
+
+# skip this if there are no modems at all
+if ! stat -t "@spoolAreaPath@"/etc/config.* >/dev/null 2>&1
+then
+ exit 0
+fi
+
+echo "faxq started, waiting for modem(s) to initialize..."
+
+for i in `seq @timeoutSec@0 -1 0` # gracefully timeout
+do
+ sleep 0.1
+ # done if status files exist, but don't mention initialization
+ if \
+ stat -t "@spoolAreaPath@"/status/* >/dev/null 2>&1 \
+ && \
+ ! grep --silent --ignore-case 'initializing server' \
+ "@spoolAreaPath@"/status/*
+ then
+ echo "modem(s) apparently ready"
+ exit 0
+ fi
+ # if i reached 0, modems probably failed to initialize
+ if test $i -eq 0
+ then
+ echo "warning: modem initialization timed out"
+ fi
+done
diff --git a/nixos/modules/services/networking/hylafax/hfaxd-default.nix b/nixos/modules/services/networking/hylafax/hfaxd-default.nix
new file mode 100644
index 000000000000..8999dae57f41
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/hfaxd-default.nix
@@ -0,0 +1,10 @@
+{ ... }:
+
+# see man:hfaxd(8)
+
+{
+
+ ServerTracing = "0x91";
+ XferLogFile = "/clientlog";
+
+}
diff --git a/nixos/modules/services/networking/hylafax/modem-default.nix b/nixos/modules/services/networking/hylafax/modem-default.nix
new file mode 100644
index 000000000000..7529b5b0aafd
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/modem-default.nix
@@ -0,0 +1,22 @@
+{ pkgs, ... }:
+
+# see man:hylafax-config(5)
+
+{
+
+ TagLineFont = "etc/LiberationSans-25.pcf";
+ TagLineLocale = ''en_US.UTF-8'';
+
+ AdminGroup = "root"; # groups that can change server config
+ AnswerRotary = "fax"; # don't accept anything else but faxes
+ LogFileMode = "0640";
+ PriorityScheduling = true;
+ RecvFileMode = "0640";
+ ServerTracing = "0x78701";
+ SessionTracing = "0x78701";
+ UUCPLockDir = "/var/lock";
+
+ SendPageCmd = ''${pkgs.coreutils}/bin/false''; # prevent pager transmit
+ SendUUCPCmd = ''${pkgs.coreutils}/bin/false''; # prevent UUCP transmit
+
+}
diff --git a/nixos/modules/services/networking/hylafax/options.nix b/nixos/modules/services/networking/hylafax/options.nix
new file mode 100644
index 000000000000..4ac6d3fa8432
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/options.nix
@@ -0,0 +1,375 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+ inherit (lib.options) literalExample mkEnableOption mkOption;
+ inherit (lib.types) bool enum int lines loaOf nullOr path str submodule;
+ inherit (lib.modules) mkDefault mkIf mkMerge;
+
+ commonDescr = ''
+ Values can be either strings or integers
+ (which will be added to the config file verbatimly)
+ or lists thereof
+ (which will be translated to multiple
+ lines with the same configuration key).
+ Boolean values are translated to "Yes" or "No".
+ The default contains some reasonable
+ configuration to yield an operational system.
+ '';
+
+ str1 = lib.types.addCheck str (s: s!=""); # non-empty string
+ int1 = lib.types.addCheck int (i: i>0); # positive integer
+
+ configAttrType =
+ # Options in HylaFAX configuration files can be
+ # booleans, strings, integers, or list thereof
+ # representing multiple config directives with the same key.
+ # This type definition resolves all
+ # those types into a list of strings.
+ let
+ inherit (lib.types) attrsOf coercedTo listOf;
+ innerType = coercedTo bool (x: if x then "Yes" else "No")
+ (coercedTo int (toString) str);
+ in
+ attrsOf (coercedTo innerType lib.singleton (listOf innerType));
+
+ cfg = config.services.hylafax;
+
+ modemConfigOptions = { name, config, ... }: {
+ options = {
+ name = mkOption {
+ type = str1;
+ example = "ttyS1";
+ description = ''
+ Name of modem device,
+ will be searched for in <filename>/dev</filename>.
+ '';
+ };
+ type = mkOption {
+ type = str1;
+ example = "cirrus";
+ description = ''
+ Name of modem configuration file,
+ will be searched for in <filename>config</filename>
+ in the spooling area directory.
+ '';
+ };
+ config = mkOption {
+ type = configAttrType;
+ example = {
+ AreaCode = "49";
+ LocalCode = "30";
+ FAXNumber = "123456";
+ LocalIdentifier = "LostInBerlin";
+ };
+ description = ''
+ Attribute set of values for the given modem.
+ ${commonDescr}
+ Options defined here override options in
+ <option>commonModemConfig</option> for this modem.
+ '';
+ };
+ };
+ config.name = mkDefault name;
+ config.config.Include = [ "config/${config.type}" ];
+ };
+
+ defaultConfig =
+ let
+ inherit (config.security) wrapperDir;
+ inherit (config.services.mail.sendmailSetuidWrapper) program;
+ mkIfDefault = cond: value: mkIf cond (mkDefault value);
+ noWrapper = config.services.mail.sendmailSetuidWrapper==null;
+ # If a sendmail setuid wrapper exists,
+ # we add the path to the default configuration file.
+ # Otherwise, we use `false` to provoke
+ # an error if hylafax tries to use it.
+ c.sendmailPath = mkMerge [
+ (mkIfDefault noWrapper ''${pkgs.coreutils}/bin/false'')
+ (mkIfDefault (!noWrapper) ''${wrapperDir}/${program}'')
+ ];
+ importDefaultConfig = file:
+ lib.attrsets.mapAttrs
+ (lib.trivial.const mkDefault)
+ (import file { inherit pkgs; });
+ c.commonModemConfig = importDefaultConfig ./modem-default.nix;
+ c.faxqConfig = importDefaultConfig ./faxq-default.nix;
+ c.hfaxdConfig = importDefaultConfig ./hfaxd-default.nix;
+ in
+ c;
+
+ localConfig =
+ let
+ c.hfaxdConfig.UserAccessFile = cfg.userAccessFile;
+ c.faxqConfig = lib.attrsets.mapAttrs
+ (lib.trivial.const (v: mkIf (v!=null) v))
+ {
+ AreaCode = cfg.areaCode;
+ CountryCode = cfg.countryCode;
+ LongDistancePrefix = cfg.longDistancePrefix;
+ InternationalPrefix = cfg.internationalPrefix;
+ };
+ c.commonModemConfig = c.faxqConfig;
+ in
+ c;
+
+in
+
+
+{
+
+
+ options.services.hylafax = {
+
+ enable = mkEnableOption ''HylaFAX server'';
+
+ autostart = mkOption {
+ type = bool;
+ default = true;
+ example = false;
+ description = ''
+ Autostart the HylaFAX queue manager at system start.
+ If this is <literal>false</literal>, the queue manager
+ will still be started if there are pending
+ jobs or if a user tries to connect to it.
+ '';
+ };
+
+ countryCode = mkOption {
+ type = nullOr str1;
+ default = null;
+ example = "49";
+ description = ''Country code for server and all modems.'';
+ };
+
+ areaCode = mkOption {
+ type = nullOr str1;
+ default = null;
+ example = "30";
+ description = ''Area code for server and all modems.'';
+ };
+
+ longDistancePrefix = mkOption {
+ type = nullOr str;
+ default = null;
+ example = "0";
+ description = ''Long distance prefix for server and all modems.'';
+ };
+
+ internationalPrefix = mkOption {
+ type = nullOr str;
+ default = null;
+ example = "00";
+ description = ''International prefix for server and all modems.'';
+ };
+
+ spoolAreaPath = mkOption {
+ type = path;
+ default = "/var/spool/fax";
+ description = ''
+ The spooling area will be created/maintained
+ at the location given here.
+ '';
+ };
+
+ userAccessFile = mkOption {
+ type = path;
+ default = "/etc/hosts.hfaxd";
+ description = ''
+ The <filename>hosts.hfaxd</filename>
+ file entry in the spooling area
+ will be symlinked to the location given here.
+ This file must exist and be
+ readable only by the <literal>uucp</literal> user.
+ See hosts.hfaxd(5) for details.
+ This configuration permits access for all users:
+ <literal>
+ environment.etc."hosts.hfaxd" = {
+ mode = "0600";
+ user = "uucp";
+ text = ".*";
+ };
+ </literal>
+ Note that host-based access can be controlled with
+ <option>config.systemd.sockets.hylafax-hfaxd.listenStreams</option>;
+ by default, only 127.0.0.1 is permitted to connect.
+ '';
+ };
+
+ sendmailPath = mkOption {
+ type = path;
+ example = literalExample "''${pkgs.postfix}/bin/sendmail";
+ # '' ; # fix vim
+ description = ''
+ Path to <filename>sendmail</filename> program.
+ The default uses the local sendmail wrapper
+ (see <option>config.services.mail.sendmailSetuidWrapper</option>),
+ otherwise the <filename>false</filename>
+ binary to cause an error if used.
+ '';
+ };
+
+ hfaxdConfig = mkOption {
+ type = configAttrType;
+ example.RecvqProtection = "0400";
+ description = ''
+ Attribute set of lines for the global
+ hfaxd config file <filename>etc/hfaxd.conf</filename>.
+ ${commonDescr}
+ '';
+ };
+
+ faxqConfig = mkOption {
+ type = configAttrType;
+ example = {
+ InternationalPrefix = "00";
+ LongDistancePrefix = "0";
+ };
+ description = ''
+ Attribute set of lines for the global
+ faxq config file <filename>etc/config</filename>.
+ ${commonDescr}
+ '';
+ };
+
+ commonModemConfig = mkOption {
+ type = configAttrType;
+ example = {
+ InternationalPrefix = "00";
+ LongDistancePrefix = "0";
+ };
+ description = ''
+ Attribute set of default values for
+ modem config files <filename>etc/config.*</filename>.
+ ${commonDescr}
+ Think twice before changing
+ paths of fax-processing scripts.
+ '';
+ };
+
+ modems = mkOption {
+ type = loaOf (submodule [ modemConfigOptions ]);
+ default = {};
+ example.ttyS1 = {
+ type = "cirrus";
+ config = {
+ FAXNumber = "123456";
+ LocalIdentifier = "Smith";
+ };
+ };
+ description = ''
+ Description of installed modems.
+ At least on modem must be defined
+ to enable the HylaFAX server.
+ '';
+ };
+
+ spoolExtraInit = mkOption {
+ type = lines;
+ default = "";
+ example = ''chmod 0755 . # everyone may read my faxes'';
+ description = ''
+ Additional shell code that is executed within the
+ spooling area directory right after its setup.
+ '';
+ };
+
+ faxcron.enable.spoolInit = mkEnableOption ''
+ Purge old files from the spooling area with
+ <filename>faxcron</filename>
+ each time the spooling area is initialized.
+ '';
+ faxcron.enable.frequency = mkOption {
+ type = nullOr str1;
+ default = null;
+ example = "daily";
+ description = ''
+ Purge old files from the spooling area with
+ <filename>faxcron</filename> with the given frequency
+ (see systemd.time(7)).
+ '';
+ };
+ faxcron.infoDays = mkOption {
+ type = int1;
+ default = 30;
+ description = ''
+ Set the expiration time for data in the
+ remote machine information directory in days.
+ '';
+ };
+ faxcron.logDays = mkOption {
+ type = int1;
+ default = 30;
+ description = ''
+ Set the expiration time for
+ session trace log files in days.
+ '';
+ };
+ faxcron.rcvDays = mkOption {
+ type = int1;
+ default = 7;
+ description = ''
+ Set the expiration time for files in
+ the received facsimile queue in days.
+ '';
+ };
+
+ faxqclean.enable.spoolInit = mkEnableOption ''
+ Purge old files from the spooling area with
+ <filename>faxqclean</filename>
+ each time the spooling area is initialized.
+ '';
+ faxqclean.enable.frequency = mkOption {
+ type = nullOr str1;
+ default = null;
+ example = "daily";
+ description = ''
+ Purge old files from the spooling area with
+ <filename>faxcron</filename> with the given frequency
+ (see systemd.time(7)).
+ '';
+ };
+ faxqclean.archiving = mkOption {
+ type = enum [ "never" "as-flagged" "always" ];
+ default = "as-flagged";
+ example = "always";
+ description = ''
+ Enable or suppress job archiving:
+ <literal>never</literal> disables job archiving,
+ <literal>as-flagged</literal> archives jobs that
+ have been flagged for archiving by sendfax,
+ <literal>always</literal> forces archiving of all jobs.
+ See also sendfax(1) and faxqclean(8).
+ '';
+ };
+ faxqclean.doneqMinutes = mkOption {
+ type = int1;
+ default = 15;
+ example = literalExample ''24*60'';
+ description = ''
+ Set the job
+ age threshold (in minutes) that controls how long
+ jobs may reside in the doneq directory.
+ '';
+ };
+ faxqclean.docqMinutes = mkOption {
+ type = int1;
+ default = 60;
+ example = literalExample ''24*60'';
+ description = ''
+ Set the document
+ age threshold (in minutes) that controls how long
+ unreferenced files may reside in the docq directory.
+ '';
+ };
+
+ };
+
+
+ config.services.hylafax =
+ mkIf
+ (config.services.hylafax.enable)
+ (mkMerge [ defaultConfig localConfig ])
+ ;
+
+}
diff --git a/nixos/modules/services/networking/hylafax/spool.sh b/nixos/modules/services/networking/hylafax/spool.sh
new file mode 100755
index 000000000000..31e930e8c597
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/spool.sh
@@ -0,0 +1,111 @@
+#! @shell@ -e
+
+# The following lines create/update the HylaFAX spool directory:
+# Subdirectories/files with persistent data are kept,
+# other directories/files are removed/recreated,
+# mostly from the template spool
+# directory in the HylaFAX package.
+
+# This block explains how the spool area is
+# derived from the spool template in the HylaFAX package:
+#
+# + capital letter: directory; file otherwise
+# + P/p: persistent directory
+# + F/f: directory with symlinks per entry
+# + T/t: temporary data
+# + S/s: single symlink into package
+# |
+# | + u: change ownership to uucp:uucp
+# | + U: ..also change access mode to user-only
+# | |
+# archive P U
+# bin S
+# client T u (client connection info)
+# config S
+# COPYRIGHT s
+# dev T u (maybe some FIFOs)
+# docq P U
+# doneq P U
+# etc F contains customized config files!
+# etc/hosts.hfaxd f
+# etc/xferfaxlog f
+# info P u (database of called devices)
+# log P u (communication logs)
+# pollq P U
+# recvq P u
+# sendq P U
+# status T u (modem status info files)
+# tmp T U
+
+
+shopt -s dotglob # if bash sees "*", it also includes dot files
+lnsym () { ln --symbol "$@" ; }
+lnsymfrc () { ln --symbolic --force "$@" ; }
+cprd () { cp --remove-destination "$@" ; }
+update () { install --owner=@faxuser@ --group=@faxgroup@ "$@" ; }
+
+
+## create/update spooling area
+
+update --mode=0750 -d "@spoolAreaPath@"
+cd "@spoolAreaPath@"
+
+persist=(archive docq doneq info log pollq recvq sendq)
+
+# remove entries that don't belong here
+touch dummy # ensure "*" resolves to something
+for k in *
+do
+ keep=0
+ for j in "${persist[@]}" xferfaxlog clientlog faxcron.lastrun
+ do
+ if test "$k" == "$j"
+ then
+ keep=1
+ break
+ fi
+ done
+ if test "$keep" == "0"
+ then
+ rm --recursive "$k"
+ fi
+done
+
+# create persistent data directories (unless they exist already)
+update --mode=0700 -d "${persist[@]}"
+chmod 0755 info log recvq
+
+# create ``xferfaxlog``, ``faxcron.lastrun``, ``clientlog``
+touch clientlog faxcron.lastrun xferfaxlog
+chown @faxuser@:@faxgroup@ clientlog faxcron.lastrun xferfaxlog
+
+# create symlinks for frozen directories/files
+lnsym --target-directory=. "@hylafax@"/spool/{COPYRIGHT,bin,config}
+
+# create empty temporary directories
+update --mode=0700 -d client dev status
+update -d tmp
+
+
+## create and fill etc
+
+install -d "@spoolAreaPath@/etc"
+cd "@spoolAreaPath@/etc"
+
+# create symlinks to all files in template's etc
+lnsym --target-directory=. "@hylafax@/spool/etc"/*
+
+# set LOCKDIR in setup.cache
+sed --regexp-extended 's|^(UUCP_LOCKDIR=).*$|\1'"'@lockPath@'|g" --in-place setup.cache
+
+# etc/{xferfaxlog,lastrun} are stored in the spool root
+lnsymfrc --target-directory=. ../xferfaxlog
+lnsymfrc --no-target-directory ../faxcron.lastrun lastrun
+
+# etc/hosts.hfaxd is provided by the NixOS configuration
+lnsymfrc --no-target-directory "@userAccessFile@" hosts.hfaxd
+
+# etc/config and etc/config.${DEVID} must be copied:
+# hfaxd reads these file after locking itself up in a chroot
+cprd --no-target-directory "@globalConfigPath@" config
+cprd --target-directory=. "@modemConfigPath@"/*
diff --git a/nixos/modules/services/networking/hylafax/systemd.nix b/nixos/modules/services/networking/hylafax/systemd.nix
new file mode 100644
index 000000000000..91d9c1a37da6
--- /dev/null
+++ b/nixos/modules/services/networking/hylafax/systemd.nix
@@ -0,0 +1,249 @@
+{ config, lib, pkgs, ... }:
+
+
+let
+
+ inherit (lib) mkIf mkMerge;
+ inherit (lib) concatStringsSep optionalString;
+
+ cfg = config.services.hylafax;
+ mapModems = lib.flip map (lib.attrValues cfg.modems);
+
+ mkConfigFile = name: conf:
+ # creates hylafax config file,
+ # makes sure "Include" is listed *first*
+ let
+ mkLines = conf:
+ (lib.concatLists
+ (lib.flip lib.mapAttrsToList conf
+ (k: map (v: ''${k}: ${v}'')
+ )));
+ include = mkLines { Include = conf.Include or []; };
+ other = mkLines ( conf // { Include = []; } );
+ in
+ pkgs.writeText ''hylafax-config${name}''
+ (concatStringsSep "\n" (include ++ other));
+
+ globalConfigPath = mkConfigFile "" cfg.faxqConfig;
+
+ modemConfigPath =
+ let
+ mkModemConfigFile = { config, name, ... }:
+ mkConfigFile ''.${name}''
+ (cfg.commonModemConfig // config);
+ mkLine = { name, type, ... }@modem: ''
+ # check if modem config file exists:
+ test -f "${pkgs.hylafaxplus}/spool/config/${type}"
+ ln \
+ --symbolic \
+ --no-target-directory \
+ "${mkModemConfigFile modem}" \
+ "$out/config.${name}"
+ '';
+ in
+ pkgs.runCommand "hylafax-config-modems" {}
+ ''mkdir --parents "$out/" ${concatStringsSep "\n" (mapModems mkLine)}'';
+
+ setupSpoolScript = pkgs.substituteAll {
+ name = "hylafax-setup-spool.sh";
+ src = ./spool.sh;
+ isExecutable = true;
+ inherit (pkgs.stdenv) shell;
+ hylafax = pkgs.hylafaxplus;
+ faxuser = "uucp";
+ faxgroup = "uucp";
+ lockPath = "/var/lock";
+ inherit globalConfigPath modemConfigPath;
+ inherit (cfg) sendmailPath spoolAreaPath userAccessFile;
+ };
+
+ waitFaxqScript = pkgs.substituteAll {
+ # This script checks the modems status files
+ # and waits until all modems report readiness.
+ name = "hylafax-faxq-wait-start.sh";
+ src = ./faxq-wait.sh;
+ isExecutable = true;
+ timeoutSec = toString 10;
+ inherit (pkgs.stdenv) shell;
+ inherit (cfg) spoolAreaPath;
+ };
+
+ sockets."hylafax-hfaxd" = {
+ description = "HylaFAX server socket";
+ documentation = [ "man:hfaxd(8)" ];
+ wantedBy = [ "multi-user.target" ];
+ listenStreams = [ "127.0.0.1:4559" ];
+ socketConfig.FreeBind = true;
+ socketConfig.Accept = true;
+ };
+
+ paths."hylafax-faxq" = {
+ description = "HylaFAX queue manager sendq watch";
+ documentation = [ "man:faxq(8)" "man:sendq(5)" ];
+ wantedBy = [ "multi-user.target" ];
+ pathConfig.PathExistsGlob = [ ''${cfg.spoolAreaPath}/sendq/q*'' ];
+ };
+
+ timers = mkMerge [
+ (
+ mkIf (cfg.faxcron.enable.frequency!=null)
+ { "hylafax-faxcron".timerConfig.Persistent = true; }
+ )
+ (
+ mkIf (cfg.faxqclean.enable.frequency!=null)
+ { "hylafax-faxqclean".timerConfig.Persistent = true; }
+ )
+ ];
+
+ hardenService =
+ # Add some common systemd service hardening settings,
+ # but allow each service (here) to override
+ # settings by explicitely setting those to `null`.
+ # More hardening would be nice but makes
+ # customizing hylafax setups very difficult.
+ # If at all, it should only be added along
+ # with some options to customize it.
+ let
+ hardening = {
+ PrivateDevices = true; # breaks /dev/tty...
+ PrivateNetwork = true;
+ PrivateTmp = true;
+ ProtectControlGroups = true;
+ #ProtectHome = true; # breaks custom spool dirs
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ #ProtectSystem = "strict"; # breaks custom spool dirs
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ };
+ filter = key: value: (value != null) || ! (lib.hasAttr key hardening);
+ apply = service: lib.filterAttrs filter (hardening // (service.serviceConfig or {}));
+ in
+ service: service // { serviceConfig = apply service; };
+
+ services."hylafax-spool" = {
+ description = "HylaFAX spool area preparation";
+ documentation = [ "man:hylafax-server(4)" ];
+ script = ''
+ ${setupSpoolScript}
+ cd "${cfg.spoolAreaPath}"
+ ${cfg.spoolExtraInit}
+ if ! test -f "${cfg.spoolAreaPath}/etc/hosts.hfaxd"
+ then
+ echo hosts.hfaxd is missing
+ exit 1
+ fi
+ '';
+ serviceConfig.ExecStop = ''${setupSpoolScript}'';
+ serviceConfig.RemainAfterExit = true;
+ serviceConfig.Type = "oneshot";
+ unitConfig.RequiresMountsFor = [ cfg.spoolAreaPath ];
+ };
+
+ services."hylafax-faxq" = {
+ description = "HylaFAX queue manager";
+ documentation = [ "man:faxq(8)" ];
+ requires = [ "hylafax-spool.service" ];
+ after = [ "hylafax-spool.service" ];
+ wants = mapModems ( { name, ... }: ''hylafax-faxgetty@${name}.service'' );
+ wantedBy = mkIf cfg.autostart [ "multi-user.target" ];
+ serviceConfig.Type = "forking";
+ serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/faxq -q "${cfg.spoolAreaPath}"'';
+ # This delays the "readiness" of this service until
+ # all modems are initialized (or a timeout is reached).
+ # Otherwise, sending a fax with the fax service
+ # stopped will always yield a failed send attempt:
+ # The fax service is started when the job is created with
+ # `sendfax`, but modems need some time to initialize.
+ serviceConfig.ExecStartPost = [ ''${waitFaxqScript}'' ];
+ # faxquit fails if the pipe is already gone
+ # (e.g. the service is already stopping)
+ serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}"'';
+ # disable some systemd hardening settings
+ serviceConfig.PrivateDevices = null;
+ serviceConfig.RestrictRealtime = null;
+ };
+
+ services."hylafax-hfaxd@" = {
+ description = "HylaFAX server";
+ documentation = [ "man:hfaxd(8)" ];
+ after = [ "hylafax-faxq.service" ];
+ requires = [ "hylafax-faxq.service" ];
+ serviceConfig.StandardInput = "socket";
+ serviceConfig.StandardOutput = "socket";
+ serviceConfig.ExecStart = ''${pkgs.hylafaxplus}/spool/bin/hfaxd -q "${cfg.spoolAreaPath}" -d -I'';
+ unitConfig.RequiresMountsFor = [ cfg.userAccessFile ];
+ # disable some systemd hardening settings
+ serviceConfig.PrivateDevices = null;
+ serviceConfig.PrivateNetwork = null;
+ };
+
+ services."hylafax-faxcron" = rec {
+ description = "HylaFAX spool area maintenance";
+ documentation = [ "man:faxcron(8)" ];
+ after = [ "hylafax-spool.service" ];
+ requires = [ "hylafax-spool.service" ];
+ wantedBy = mkIf cfg.faxcron.enable.spoolInit requires;
+ startAt = mkIf (cfg.faxcron.enable.frequency!=null) cfg.faxcron.enable.frequency;
+ serviceConfig.ExecStart = concatStringsSep " " [
+ ''${pkgs.hylafaxplus}/spool/bin/faxcron''
+ ''-q "${cfg.spoolAreaPath}"''
+ ''-info ${toString cfg.faxcron.infoDays}''
+ ''-log ${toString cfg.faxcron.logDays}''
+ ''-rcv ${toString cfg.faxcron.rcvDays}''
+ ];
+ };
+
+ services."hylafax-faxqclean" = rec {
+ description = "HylaFAX spool area queue cleaner";
+ documentation = [ "man:faxqclean(8)" ];
+ after = [ "hylafax-spool.service" ];
+ requires = [ "hylafax-spool.service" ];
+ wantedBy = mkIf cfg.faxqclean.enable.spoolInit requires;
+ startAt = mkIf (cfg.faxqclean.enable.frequency!=null) cfg.faxqclean.enable.frequency;
+ serviceConfig.ExecStart = concatStringsSep " " [
+ ''${pkgs.hylafaxplus}/spool/bin/faxqclean''
+ ''-q "${cfg.spoolAreaPath}"''
+ ''-v''
+ (optionalString (cfg.faxqclean.archiving!="never") ''-a'')
+ (optionalString (cfg.faxqclean.archiving=="always") ''-A'')
+ ''-j ${toString (cfg.faxqclean.doneqMinutes*60)}''
+ ''-d ${toString (cfg.faxqclean.docqMinutes*60)}''
+ ];
+ };
+
+ mkFaxgettyService = { name, ... }:
+ lib.nameValuePair ''hylafax-faxgetty@${name}'' rec {
+ description = "HylaFAX faxgetty for %I";
+ documentation = [ "man:faxgetty(8)" ];
+ bindsTo = [ "dev-%i.device" ];
+ requires = [ "hylafax-spool.service" ];
+ after = bindsTo ++ requires;
+ before = [ "hylafax-faxq.service" "getty.target" ];
+ unitConfig.StopWhenUnneeded = true;
+ unitConfig.AssertFileNotEmpty = ''${cfg.spoolAreaPath}/etc/config.%I'';
+ serviceConfig.UtmpIdentifier = "%I";
+ serviceConfig.TTYPath = "/dev/%I";
+ serviceConfig.Restart = "always";
+ serviceConfig.KillMode = "process";
+ serviceConfig.IgnoreSIGPIPE = false;
+ serviceConfig.ExecStart = ''-${pkgs.hylafaxplus}/spool/bin/faxgetty -q "${cfg.spoolAreaPath}" /dev/%I'';
+ # faxquit fails if the pipe is already gone
+ # (e.g. the service is already stopping)
+ serviceConfig.ExecStop = ''-${pkgs.hylafaxplus}/spool/bin/faxquit -q "${cfg.spoolAreaPath}" %I'';
+ # disable some systemd hardening settings
+ serviceConfig.PrivateDevices = null;
+ serviceConfig.RestrictRealtime = null;
+ };
+
+ modemServices =
+ lib.listToAttrs (mapModems mkFaxgettyService);
+
+in
+
+{
+ config.systemd = mkIf cfg.enable {
+ inherit sockets timers paths;
+ services = lib.mapAttrs (lib.const hardenService) (services // modemServices);
+ };
+}