at 24.11-pre 15 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 inherit (pkgs) cups-pk-helper cups-filters xdg-utils; 8 9 cfg = config.services.printing; 10 cups = cfg.package; 11 12 avahiEnabled = config.services.avahi.enable; 13 polkitEnabled = config.security.polkit.enable; 14 15 additionalBackends = pkgs.runCommand "additional-cups-backends" { 16 preferLocalBuild = true; 17 } '' 18 mkdir -p $out 19 if [ ! -e ${cups.out}/lib/cups/backend/smb ]; then 20 mkdir -p $out/lib/cups/backend 21 ln -sv ${pkgs.samba}/bin/smbspool $out/lib/cups/backend/smb 22 fi 23 24 # Provide support for printing via HTTPS. 25 if [ ! -e ${cups.out}/lib/cups/backend/https ]; then 26 mkdir -p $out/lib/cups/backend 27 ln -sv ${cups.out}/lib/cups/backend/ipp $out/lib/cups/backend/https 28 fi 29 ''; 30 31 # Here we can enable additional backends, filters, etc. that are not 32 # part of CUPS itself, e.g. the SMB backend is part of Samba. Since 33 # we can't update ${cups.out}/lib/cups itself, we create a symlink tree 34 # here and add the additional programs. The ServerBin directive in 35 # cups-files.conf tells cupsd to use this tree. 36 bindir = pkgs.buildEnv { 37 name = "cups-progs"; 38 paths = 39 [ cups.out additionalBackends cups-filters pkgs.ghostscript ] 40 ++ cfg.drivers; 41 pathsToLink = [ "/lib" "/share/cups" "/bin" ]; 42 postBuild = cfg.bindirCmds; 43 ignoreCollisions = true; 44 }; 45 46 writeConf = name: text: pkgs.writeTextFile { 47 inherit name text; 48 destination = "/etc/cups/${name}"; 49 }; 50 51 cupsFilesFile = writeConf "cups-files.conf" '' 52 SystemGroup root wheel 53 54 ServerBin ${bindir}/lib/cups 55 DataDir ${bindir}/share/cups 56 DocumentRoot ${cups.out}/share/doc/cups 57 58 AccessLog syslog 59 ErrorLog syslog 60 PageLog syslog 61 62 TempDir ${cfg.tempDir} 63 64 SetEnv PATH /var/lib/cups/path/lib/cups/filter:/var/lib/cups/path/bin 65 66 # User and group used to run external programs, including 67 # those that actually send the job to the printer. Note that 68 # Udev sets the group of printer devices to `lp', so we want 69 # these programs to run as `lp' as well. 70 User cups 71 Group lp 72 73 ${cfg.extraFilesConf} 74 ''; 75 76 cupsdFile = writeConf "cupsd.conf" '' 77 ${concatMapStrings (addr: '' 78 Listen ${addr} 79 '') cfg.listenAddresses} 80 Listen /run/cups/cups.sock 81 82 DefaultShared ${if cfg.defaultShared then "Yes" else "No"} 83 84 Browsing ${if cfg.browsing then "Yes" else "No"} 85 86 WebInterface ${if cfg.webInterface then "Yes" else "No"} 87 88 LogLevel ${cfg.logLevel} 89 90 ${cfg.extraConf} 91 ''; 92 93 browsedFile = writeConf "cups-browsed.conf" cfg.browsedConf; 94 95 rootdir = pkgs.buildEnv { 96 name = "cups-progs"; 97 paths = [ 98 cupsFilesFile 99 cupsdFile 100 (writeConf "client.conf" cfg.clientConf) 101 (writeConf "snmp.conf" cfg.snmpConf) 102 ] ++ optional avahiEnabled browsedFile 103 ++ cfg.drivers; 104 pathsToLink = [ "/etc/cups" ]; 105 ignoreCollisions = true; 106 }; 107 108 filterGutenprint = filter (pkg: pkg.meta.isGutenprint or false == true); 109 containsGutenprint = pkgs: length (filterGutenprint pkgs) > 0; 110 getGutenprint = pkgs: head (filterGutenprint pkgs); 111 112 parsePorts = addresses: let 113 splitAddress = addr: strings.splitString ":" addr; 114 extractPort = addr: builtins.foldl' (a: b: b) "" (splitAddress addr); 115 in 116 builtins.map (address: strings.toInt (extractPort address)) addresses; 117 118in 119 120{ 121 122 imports = [ 123 (mkChangedOptionModule [ "services" "printing" "gutenprint" ] [ "services" "printing" "drivers" ] 124 (config: 125 let enabled = getAttrFromPath [ "services" "printing" "gutenprint" ] config; 126 in if enabled then [ pkgs.gutenprint ] else [ ])) 127 (mkRemovedOptionModule [ "services" "printing" "cupsFilesConf" ] "") 128 (mkRemovedOptionModule [ "services" "printing" "cupsdConf" ] "") 129 ]; 130 131 ###### interface 132 133 options = { 134 services.printing = { 135 136 enable = mkOption { 137 type = types.bool; 138 default = false; 139 description = '' 140 Whether to enable printing support through the CUPS daemon. 141 ''; 142 }; 143 144 package = lib.mkPackageOption pkgs "cups" {}; 145 146 stateless = mkOption { 147 type = types.bool; 148 default = false; 149 description = '' 150 If set, all state directories relating to CUPS will be removed on 151 startup of the service. 152 ''; 153 }; 154 155 startWhenNeeded = mkOption { 156 type = types.bool; 157 default = true; 158 description = '' 159 If set, CUPS is socket-activated; that is, 160 instead of having it permanently running as a daemon, 161 systemd will start it on the first incoming connection. 162 ''; 163 }; 164 165 listenAddresses = mkOption { 166 type = types.listOf types.str; 167 default = [ "localhost:631" ]; 168 example = [ "*:631" ]; 169 description = '' 170 A list of addresses and ports on which to listen. 171 ''; 172 }; 173 174 allowFrom = mkOption { 175 type = types.listOf types.str; 176 default = [ "localhost" ]; 177 example = [ "all" ]; 178 apply = concatMapStringsSep "\n" (x: "Allow ${x}"); 179 description = '' 180 From which hosts to allow unconditional access. 181 ''; 182 }; 183 184 openFirewall = mkOption { 185 type = types.bool; 186 default = false; 187 description = '' 188 Whether to open the firewall for TCP/UDP ports specified in 189 listenAdrresses option. 190 ''; 191 }; 192 193 bindirCmds = mkOption { 194 type = types.lines; 195 internal = true; 196 default = ""; 197 description = '' 198 Additional commands executed while creating the directory 199 containing the CUPS server binaries. 200 ''; 201 }; 202 203 defaultShared = mkOption { 204 type = types.bool; 205 default = false; 206 description = '' 207 Specifies whether local printers are shared by default. 208 ''; 209 }; 210 211 browsing = mkOption { 212 type = types.bool; 213 default = false; 214 description = '' 215 Specifies whether shared printers are advertised. 216 ''; 217 }; 218 219 webInterface = mkOption { 220 type = types.bool; 221 default = true; 222 description = '' 223 Specifies whether the web interface is enabled. 224 ''; 225 }; 226 227 logLevel = mkOption { 228 type = types.str; 229 default = "info"; 230 example = "debug"; 231 description = '' 232 Specifies the cupsd logging verbosity. 233 ''; 234 }; 235 236 extraFilesConf = mkOption { 237 type = types.lines; 238 default = ""; 239 description = '' 240 Extra contents of the configuration file of the CUPS daemon 241 ({file}`cups-files.conf`). 242 ''; 243 }; 244 245 extraConf = mkOption { 246 type = types.lines; 247 default = ""; 248 example = 249 '' 250 BrowsePoll cups.example.com 251 MaxCopies 42 252 ''; 253 description = '' 254 Extra contents of the configuration file of the CUPS daemon 255 ({file}`cupsd.conf`). 256 ''; 257 }; 258 259 clientConf = mkOption { 260 type = types.lines; 261 default = ""; 262 example = 263 '' 264 ServerName server.example.com 265 Encryption Never 266 ''; 267 description = '' 268 The contents of the client configuration. 269 ({file}`client.conf`) 270 ''; 271 }; 272 273 browsedConf = mkOption { 274 type = types.lines; 275 default = ""; 276 example = 277 '' 278 BrowsePoll cups.example.com 279 ''; 280 description = '' 281 The contents of the configuration. file of the CUPS Browsed daemon 282 ({file}`cups-browsed.conf`) 283 ''; 284 }; 285 286 snmpConf = mkOption { 287 type = types.lines; 288 default = '' 289 Address @LOCAL 290 ''; 291 description = '' 292 The contents of {file}`/etc/cups/snmp.conf`. See "man 293 cups-snmp.conf" for a complete description. 294 ''; 295 }; 296 297 drivers = mkOption { 298 type = types.listOf types.path; 299 default = []; 300 example = literalExpression "with pkgs; [ gutenprint hplip splix ]"; 301 description = '' 302 CUPS drivers to use. Drivers provided by CUPS, cups-filters, 303 Ghostscript and Samba are added unconditionally. If this list contains 304 Gutenprint (i.e. a derivation with 305 `meta.isGutenprint = true`) the PPD files in 306 {file}`/var/lib/cups/ppd` will be updated automatically 307 to avoid errors due to incompatible versions. 308 ''; 309 }; 310 311 tempDir = mkOption { 312 type = types.path; 313 default = "/tmp"; 314 example = "/tmp/cups"; 315 description = '' 316 CUPSd temporary directory. 317 ''; 318 }; 319 }; 320 321 }; 322 323 324 ###### implementation 325 326 config = mkIf config.services.printing.enable { 327 328 users.users.cups = 329 { uid = config.ids.uids.cups; 330 group = "lp"; 331 description = "CUPS printing services"; 332 }; 333 334 # We need xdg-open (part of xdg-utils) for the desktop-file to proper open the users default-browser when opening "Manage Printing" 335 # https://github.com/NixOS/nixpkgs/pull/237994#issuecomment-1597510969 336 environment.systemPackages = [ cups.out xdg-utils ] ++ optional polkitEnabled cups-pk-helper; 337 environment.etc.cups.source = "/var/lib/cups"; 338 339 services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper; 340 services.udev.packages = cfg.drivers; 341 342 # Allow asswordless printer admin for members of wheel group 343 security.polkit.extraConfig = mkIf polkitEnabled '' 344 polkit.addRule(function(action, subject) { 345 if (action.id == "org.opensuse.cupspkhelper.mechanism.all-edit" && 346 subject.isInGroup("wheel")){ 347 return polkit.Result.YES; 348 } 349 }); 350 ''; 351 352 # Cups uses libusb to talk to printers, and does not use the 353 # linux kernel driver. If the driver is not in a black list, it 354 # gets loaded, and then cups cannot access the printers. 355 boot.blacklistedKernelModules = [ "usblp" ]; 356 357 # Some programs like print-manager rely on this value to get 358 # printer test pages. 359 environment.sessionVariables.CUPS_DATADIR = "${bindir}/share/cups"; 360 361 systemd.packages = [ cups.out ]; 362 363 systemd.sockets.cups = mkIf cfg.startWhenNeeded { 364 wantedBy = [ "sockets.target" ]; 365 listenStreams = [ "" "/run/cups/cups.sock" ] 366 ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses; 367 }; 368 369 systemd.services.cups = 370 { wantedBy = optionals (!cfg.startWhenNeeded) [ "multi-user.target" ]; 371 wants = [ "network.target" ]; 372 after = [ "network.target" ]; 373 374 path = [ cups.out ]; 375 376 preStart = lib.optionalString cfg.stateless '' 377 rm -rf /var/cache/cups /var/lib/cups /var/spool/cups 378 '' + '' 379 mkdir -m 0700 -p /var/cache/cups 380 mkdir -m 0700 -p /var/spool/cups 381 mkdir -m 0755 -p ${cfg.tempDir} 382 383 mkdir -m 0755 -p /var/lib/cups 384 # While cups will automatically create self-signed certificates if accessed via TLS, 385 # this directory to store the certificates needs to be created manually. 386 mkdir -m 0700 -p /var/lib/cups/ssl 387 388 # Backwards compatibility 389 if [ ! -L /etc/cups ]; then 390 mv /etc/cups/* /var/lib/cups 391 rmdir /etc/cups 392 ln -s /var/lib/cups /etc/cups 393 fi 394 # First, clean existing symlinks 395 if [ -n "$(ls /var/lib/cups)" ]; then 396 for i in /var/lib/cups/*; do 397 [ -L "$i" ] && rm "$i" 398 done 399 fi 400 # Then, populate it with static files 401 cd ${rootdir}/etc/cups 402 for i in *; do 403 [ ! -e "/var/lib/cups/$i" ] && ln -s "${rootdir}/etc/cups/$i" "/var/lib/cups/$i" 404 done 405 406 #update path reference 407 [ -L /var/lib/cups/path ] && \ 408 rm /var/lib/cups/path 409 [ ! -e /var/lib/cups/path ] && \ 410 ln -s ${bindir} /var/lib/cups/path 411 412 ${optionalString (containsGutenprint cfg.drivers) '' 413 if [ -d /var/lib/cups/ppd ]; then 414 ${getGutenprint cfg.drivers}/bin/cups-genppdupdate -p /var/lib/cups/ppd 415 fi 416 ''} 417 ''; 418 419 serviceConfig.PrivateTmp = true; 420 }; 421 422 systemd.services.cups-browsed = mkIf avahiEnabled 423 { description = "CUPS Remote Printer Discovery"; 424 425 wantedBy = [ "multi-user.target" ]; 426 wants = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 427 bindsTo = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 428 partOf = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 429 after = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 430 431 path = [ cups ]; 432 433 serviceConfig.ExecStart = "${cups-filters}/bin/cups-browsed"; 434 435 restartTriggers = [ browsedFile ]; 436 }; 437 438 services.printing.extraConf = 439 '' 440 DefaultAuthType Basic 441 442 <Location /> 443 Order allow,deny 444 ${cfg.allowFrom} 445 </Location> 446 447 <Location /admin> 448 Order allow,deny 449 ${cfg.allowFrom} 450 </Location> 451 452 <Location /admin/conf> 453 AuthType Basic 454 Require user @SYSTEM 455 Order allow,deny 456 ${cfg.allowFrom} 457 </Location> 458 459 <Policy default> 460 <Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job CUPS-Move-Job> 461 Require user @OWNER @SYSTEM 462 Order deny,allow 463 </Limit> 464 465 <Limit Pause-Printer Resume-Printer Set-Printer-Attributes Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After CUPS-Add-Printer CUPS-Delete-Printer CUPS-Add-Class CUPS-Delete-Class CUPS-Accept-Jobs CUPS-Reject-Jobs CUPS-Set-Default> 466 AuthType Basic 467 Require user @SYSTEM 468 Order deny,allow 469 </Limit> 470 471 <Limit Cancel-Job CUPS-Authenticate-Job> 472 Require user @OWNER @SYSTEM 473 Order deny,allow 474 </Limit> 475 476 <Limit All> 477 Order deny,allow 478 </Limit> 479 </Policy> 480 ''; 481 482 security.pam.services.cups = {}; 483 484 networking.firewall = let 485 listenPorts = parsePorts cfg.listenAddresses; 486 in mkIf cfg.openFirewall { 487 allowedTCPPorts = listenPorts; 488 allowedUDPPorts = listenPorts; 489 }; 490 491 }; 492 493 meta.maintainers = with lib.maintainers; [ matthewbauer ]; 494 495}