1{ config, lib, pkgs, ... }:
2
3let
4
5 # cups calls its backends as user `lp` (which is good!),
6 # but cups-pdf wants to be called as `root`, so it can change ownership of files.
7 # We add a suid wrapper and a wrapper script to trick cups into calling the suid wrapper.
8 # Note that a symlink to the suid wrapper alone wouldn't suffice, cups would complain
9 # > File "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-cups-progs/lib/cups/backend/cups-pdf" has insecure permissions (0104554/uid=0/gid=20)
10
11 # wrapper script that redirects calls to the suid wrapper
12 cups-pdf-wrapper = pkgs.writeTextFile {
13 name = "${pkgs.cups-pdf-to-pdf.name}-wrapper.sh";
14 executable = true;
15 destination = "/lib/cups/backend/cups-pdf";
16 checkPhase = ''
17 ${pkgs.stdenv.shellDryRun} "$target"
18 ${lib.getExe pkgs.shellcheck} "$target"
19 '';
20 text = ''
21 #! ${pkgs.runtimeShell}
22 exec "${config.security.wrapperDir}/cups-pdf" "$@"
23 '';
24 };
25
26 # wrapped cups-pdf package that uses the suid wrapper
27 cups-pdf-wrapped = pkgs.buildEnv {
28 name = "${pkgs.cups-pdf-to-pdf.name}-wrapped";
29 # using the wrapper as first path ensures it is used
30 paths = [ cups-pdf-wrapper pkgs.cups-pdf-to-pdf ];
31 ignoreCollisions = true;
32 };
33
34 instanceSettings = name: {
35 freeformType = with lib.types; nullOr (oneOf [ int str path package ]);
36 # override defaults:
37 # inject instance name into paths,
38 # also avoid conflicts between user names and special dirs
39 options.Out = lib.mkOption {
40 type = with lib.types; nullOr singleLineStr;
41 default = "/var/spool/cups-pdf-${name}/users/\${USER}";
42 defaultText = "/var/spool/cups-pdf-{instance-name}/users/\${USER}";
43 example = "\${HOME}/cups-pdf";
44 description = ''
45 output directory;
46 `''${HOME}` will be expanded to the user's home directory,
47 `''${USER}` will be expanded to the user name.
48 '';
49 };
50 options.AnonDirName = lib.mkOption {
51 type = with lib.types; nullOr singleLineStr;
52 default = "/var/spool/cups-pdf-${name}/anonymous";
53 defaultText = "/var/spool/cups-pdf-{instance-name}/anonymous";
54 example = "/var/lib/cups-pdf";
55 description = "path for anonymously created PDF files";
56 };
57 options.Spool = lib.mkOption {
58 type = with lib.types; nullOr singleLineStr;
59 default = "/var/spool/cups-pdf-${name}/spool";
60 defaultText = "/var/spool/cups-pdf-{instance-name}/spool";
61 example = "/var/lib/cups-pdf";
62 description = "spool directory";
63 };
64 options.Anonuser = lib.mkOption {
65 type = lib.types.singleLineStr;
66 default = "root";
67 description = ''
68 User for anonymous PDF creation.
69 An empty string disables this feature.
70 '';
71 };
72 options.GhostScript = lib.mkOption {
73 type = with lib.types; nullOr path;
74 default = lib.getExe pkgs.ghostscript;
75 defaultText = lib.literalExpression "lib.getExe pkgs.ghostscript";
76 example = lib.literalExpression ''''${pkgs.ghostscript}/bin/ps2pdf'';
77 description = "location of GhostScript binary";
78 };
79 };
80
81 instanceConfig = { name, config, ... }: {
82 options = {
83 enable = (lib.mkEnableOption "this cups-pdf instance") // { default = true; };
84 installPrinter = (lib.mkEnableOption ''
85 a CUPS printer queue for this instance.
86 The queue will be named after the instance and will use the {file}`CUPS-PDF_opt.ppd` ppd file.
87 If this is disabled, you need to add the queue yourself to use the instance
88 '') // { default = true; };
89 confFileText = lib.mkOption {
90 type = lib.types.lines;
91 description = ''
92 This will contain the contents of {file}`cups-pdf.conf` for this instance, derived from {option}`settings`.
93 You can use this option to append text to the file.
94 '';
95 };
96 settings = lib.mkOption {
97 type = lib.types.submodule (instanceSettings name);
98 default = {};
99 example = {
100 Out = "\${HOME}/cups-pdf";
101 UserUMask = "0033";
102 };
103 description = ''
104 Settings for a cups-pdf instance, see the descriptions in the template config file in the cups-pdf package.
105 The key value pairs declared here will be translated into proper key value pairs for {file}`cups-pdf.conf`.
106 Setting a value to `null` disables the option and removes it from the file.
107 '';
108 };
109 };
110 config.confFileText = lib.pipe config.settings [
111 (lib.filterAttrs (key: value: value != null))
112 (lib.mapAttrs (key: builtins.toString))
113 (lib.mapAttrsToList (key: value: "${key} ${value}\n"))
114 lib.concatStrings
115 ];
116 };
117
118 cupsPdfCfg = config.services.printing.cups-pdf;
119
120 copyConfigFileCmds = lib.pipe cupsPdfCfg.instances [
121 (lib.filterAttrs (name: lib.getAttr "enable"))
122 (lib.mapAttrs (name: lib.getAttr "confFileText"))
123 (lib.mapAttrs (name: pkgs.writeText "cups-pdf-${name}.conf"))
124 (lib.mapAttrsToList (name: confFile: "ln --symbolic --no-target-directory ${confFile} /var/lib/cups/cups-pdf-${name}.conf\n"))
125 lib.concatStrings
126 ];
127
128 printerSettings = lib.pipe cupsPdfCfg.instances [
129 (lib.filterAttrs (name: lib.getAttr "enable"))
130 (lib.filterAttrs (name: lib.getAttr "installPrinter"))
131 (lib.mapAttrsToList (name: instance: (lib.mapAttrs (key: lib.mkDefault) {
132 inherit name;
133 model = "CUPS-PDF_opt.ppd";
134 deviceUri = "cups-pdf:/${name}";
135 description = "virtual printer for cups-pdf instance ${name}";
136 location = instance.settings.Out;
137 })))
138 ];
139
140in
141
142{
143
144 options.services.printing.cups-pdf = {
145 enable = lib.mkEnableOption ''
146 the cups-pdf virtual pdf printer backend.
147 By default, this will install a single printer `pdf`.
148 but this can be changed/extended with {option}`services.printing.cups-pdf.instances`
149 '';
150 instances = lib.mkOption {
151 type = lib.types.attrsOf (lib.types.submodule instanceConfig);
152 default.pdf = {};
153 example.pdf.settings = {
154 Out = "\${HOME}/cups-pdf";
155 UserUMask = "0033";
156 };
157 description = ''
158 Permits to raise one or more cups-pdf instances.
159 Each instance is named by an attribute name, and the attribute's values control the instance' configuration.
160 '';
161 };
162 };
163
164 config = lib.mkIf cupsPdfCfg.enable {
165 services.printing.enable = true;
166 services.printing.drivers = [ cups-pdf-wrapped ];
167 hardware.printers.ensurePrinters = printerSettings;
168 # the cups module will install the default config file,
169 # but we don't need it and it would confuse cups-pdf
170 systemd.services.cups.preStart = lib.mkAfter ''
171 rm -f /var/lib/cups/cups-pdf.conf
172 ${copyConfigFileCmds}
173 '';
174 security.wrappers.cups-pdf = {
175 group = "lp";
176 owner = "root";
177 permissions = "+r,ug+x";
178 setuid = true;
179 source = "${pkgs.cups-pdf-to-pdf}/lib/cups/backend/cups-pdf";
180 };
181 };
182
183 meta.maintainers = [ lib.maintainers.yarny ];
184
185}