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