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