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.wip/yesman
parent
a08b633fe7
commit
12fa95f2d6
@ -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>. |
||||
''; |
||||
}]; |
||||
}; |
||||
|
||||
} |
@ -0,0 +1,12 @@ |
||||
{ ... }: |
||||
|
||||
# see man:hylafax-config(5) |
||||
|
||||
{ |
||||
|
||||
ModemGroup = [ ''"any:.*"'' ]; |
||||
ServerTracing = "0x78701"; |
||||
SessionTracing = "0x78701"; |
||||
UUCPLockDir = "/var/lock"; |
||||
|
||||
} |
@ -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 |
@ -0,0 +1,10 @@ |
||||
{ ... }: |
||||
|
||||
# see man:hfaxd(8) |
||||
|
||||
{ |
||||
|
||||
ServerTracing = "0x91"; |
||||
XferLogFile = "/clientlog"; |
||||
|
||||
} |
@ -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 |
||||
|
||||
} |
@ -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 ]) |
||||
; |
||||
|
||||
} |
@ -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@"/* |
@ -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); |
||||
}; |
||||
} |
Loading…
Reference in new issue